You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

385 lines
15 KiB

  1. module ActiveMerchant #:nodoc:
  2. module Billing #:nodoc:
  3. # This module is included in both PaypalGateway and PaypalExpressGateway
  4. module PaypalCommonAPI
  5. def self.included(base)
  6. base.default_currency = 'USD'
  7. base.cattr_accessor :pem_file
  8. base.cattr_accessor :signature
  9. end
  10. API_VERSION = '57.0' # TODO - check absolute adherence in this file, override in sub?
  11. URLS = {
  12. :test => { :certificate => 'https://api.sandbox.paypal.com/2.0/',
  13. :signature => 'https://api-3t.sandbox.paypal.com/2.0/' },
  14. :live => { :certificate => 'https://api-aa.paypal.com/2.0/',
  15. :signature => 'https://api-3t.paypal.com/2.0/' }
  16. }
  17. PAYPAL_NAMESPACE = 'urn:ebay:api:PayPalAPI'
  18. EBAY_NAMESPACE = 'urn:ebay:apis:eBLBaseComponents'
  19. ENVELOPE_NAMESPACES = { 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema',
  20. 'xmlns:env' => 'http://schemas.xmlsoap.org/soap/envelope/',
  21. 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
  22. }
  23. CREDENTIALS_NAMESPACES = { 'xmlns' => PAYPAL_NAMESPACE,
  24. 'xmlns:n1' => EBAY_NAMESPACE,
  25. 'env:mustUnderstand' => '0'
  26. }
  27. AUSTRALIAN_STATES = {
  28. 'ACT' => 'Australian Capital Territory',
  29. 'NSW' => 'New South Wales',
  30. 'NT' => 'Northern Territory',
  31. 'QLD' => 'Queensland',
  32. 'SA' => 'South Australia',
  33. 'TAS' => 'Tasmania',
  34. 'VIC' => 'Victoria',
  35. 'WA' => 'Western Australia'
  36. }
  37. SUCCESS_CODES = [ 'Success', 'SuccessWithWarning' ]
  38. FRAUD_REVIEW_CODE = "11610"
  39. # The gateway must be configured with either your PayPal PEM file
  40. # or your PayPal API Signature. Only one is required.
  41. #
  42. # <tt>:pem</tt> The text of your PayPal PEM file. Note
  43. # this is not the path to file, but its
  44. # contents. If you are only using one PEM
  45. # file on your site you can declare it
  46. # globally and then you won't need to
  47. # include this option
  48. #
  49. # <tt>:signature</tt> The text of your PayPal signature.
  50. # If you are only using one API Signature
  51. # on your site you can declare it
  52. # globally and then you won't need to
  53. # include this option
  54. def initialize(options = {})
  55. requires!(options, :login, :password)
  56. @options = {
  57. :pem => pem_file,
  58. :signature => signature
  59. }.update(options)
  60. if @options[:pem].blank? && @options[:signature].blank?
  61. raise ArgumentError, "An API Certificate or API Signature is required to make requests to PayPal"
  62. end
  63. super
  64. end
  65. def test?
  66. @options[:test] || Base.gateway_mode == :test
  67. end
  68. def reauthorize(money, authorization, options = {})
  69. commit 'DoReauthorization', build_reauthorize_request(money, authorization, options)
  70. end
  71. def capture(money, authorization, options = {})
  72. commit 'DoCapture', build_capture_request(money, authorization, options)
  73. end
  74. # Transfer money to one or more recipients.
  75. #
  76. # gateway.transfer 1000, 'bob@example.com',
  77. # :subject => "The money I owe you", :note => "Sorry it's so late"
  78. #
  79. # gateway.transfer [1000, 'fred@example.com'],
  80. # [2450, 'wilma@example.com', :note => 'You will receive another payment on 3/24'],
  81. # [2000, 'barney@example.com'],
  82. # :subject => "Your Earnings", :note => "Thanks for your business."
  83. #
  84. def transfer(*args)
  85. commit 'MassPay', build_mass_pay_request(*args)
  86. end
  87. def void(authorization, options = {})
  88. commit 'DoVoid', build_void_request(authorization, options)
  89. end
  90. def credit(money, identification, options = {})
  91. commit 'RefundTransaction', build_credit_request(money, identification, options)
  92. end
  93. private
  94. def build_reauthorize_request(money, authorization, options)
  95. xml = Builder::XmlMarkup.new
  96. xml.tag! 'DoReauthorizationReq', 'xmlns' => PAYPAL_NAMESPACE do
  97. xml.tag! 'DoReauthorizationRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  98. xml.tag! 'n2:Version', API_VERSION
  99. xml.tag! 'AuthorizationID', authorization
  100. xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
  101. end
  102. end
  103. xml.target!
  104. end
  105. def build_capture_request(money, authorization, options)
  106. xml = Builder::XmlMarkup.new
  107. xml.tag! 'DoCaptureReq', 'xmlns' => PAYPAL_NAMESPACE do
  108. xml.tag! 'DoCaptureRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  109. xml.tag! 'n2:Version', API_VERSION
  110. xml.tag! 'AuthorizationID', authorization
  111. xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
  112. xml.tag! 'CompleteType', 'Complete'
  113. xml.tag! 'Note', options[:description]
  114. end
  115. end
  116. xml.target!
  117. end
  118. def build_credit_request(money, identification, options)
  119. xml = Builder::XmlMarkup.new
  120. xml.tag! 'RefundTransactionReq', 'xmlns' => PAYPAL_NAMESPACE do
  121. xml.tag! 'RefundTransactionRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  122. xml.tag! 'n2:Version', API_VERSION
  123. xml.tag! 'TransactionID', identification
  124. xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
  125. xml.tag! 'RefundType', 'Partial'
  126. xml.tag! 'Memo', options[:note] unless options[:note].blank?
  127. end
  128. end
  129. xml.target!
  130. end
  131. def build_void_request(authorization, options)
  132. xml = Builder::XmlMarkup.new
  133. xml.tag! 'DoVoidReq', 'xmlns' => PAYPAL_NAMESPACE do
  134. xml.tag! 'DoVoidRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  135. xml.tag! 'n2:Version', API_VERSION
  136. xml.tag! 'AuthorizationID', authorization
  137. xml.tag! 'Note', options[:description]
  138. end
  139. end
  140. xml.target!
  141. end
  142. def build_mass_pay_request(*args)
  143. default_options = args.last.is_a?(Hash) ? args.pop : {}
  144. recipients = args.first.is_a?(Array) ? args : [args]
  145. xml = Builder::XmlMarkup.new
  146. xml.tag! 'MassPayReq', 'xmlns' => PAYPAL_NAMESPACE do
  147. xml.tag! 'MassPayRequest', 'xmlns:n2' => EBAY_NAMESPACE do
  148. xml.tag! 'n2:Version', API_VERSION
  149. xml.tag! 'EmailSubject', default_options[:subject] if default_options[:subject]
  150. recipients.each do |money, recipient, options|
  151. options ||= default_options
  152. xml.tag! 'MassPayItem' do
  153. xml.tag! 'ReceiverEmail', recipient
  154. xml.tag! 'Amount', amount(money), 'currencyID' => options[:currency] || currency(money)
  155. xml.tag! 'Note', options[:note] if options[:note]
  156. xml.tag! 'UniqueId', options[:unique_id] if options[:unique_id]
  157. end
  158. end
  159. end
  160. end
  161. xml.target!
  162. end
  163. def parse(action, xml)
  164. response = {}
  165. error_messages = []
  166. error_codes = []
  167. xml = REXML::Document.new(xml)
  168. if root = REXML::XPath.first(xml, "//#{action}Response")
  169. root.elements.each do |node|
  170. case node.name
  171. when 'Errors'
  172. short_message = nil
  173. long_message = nil
  174. node.elements.each do |child|
  175. case child.name
  176. when "LongMessage"
  177. long_message = child.text unless child.text.blank?
  178. when "ShortMessage"
  179. short_message = child.text unless child.text.blank?
  180. when "ErrorCode"
  181. error_codes << child.text unless child.text.blank?
  182. end
  183. end
  184. if message = long_message || short_message
  185. error_messages << message
  186. end
  187. else
  188. parse_element(response, node)
  189. end
  190. end
  191. response[:message] = error_messages.uniq.join(". ") unless error_messages.empty?
  192. response[:error_codes] = error_codes.uniq.join(",") unless error_codes.empty?
  193. elsif root = REXML::XPath.first(xml, "//SOAP-ENV:Fault")
  194. parse_element(response, root)
  195. response[:message] = "#{response[:faultcode]}: #{response[:faultstring]} - #{response[:detail]}"
  196. end
  197. response
  198. end
  199. def parse_element(response, node)
  200. if node.has_elements?
  201. node.elements.each{|e| parse_element(response, e) }
  202. else
  203. response[node.name.underscore.to_sym] = node.text
  204. node.attributes.each do |k, v|
  205. response["#{node.name.underscore}_#{k.underscore}".to_sym] = v if k == 'currencyID'
  206. end
  207. end
  208. end
  209. def build_request(body)
  210. xml = Builder::XmlMarkup.new
  211. xml.instruct!
  212. xml.tag! 'env:Envelope', ENVELOPE_NAMESPACES do
  213. xml.tag! 'env:Header' do
  214. add_credentials(xml)
  215. end
  216. xml.tag! 'env:Body' do
  217. xml << body
  218. end
  219. end
  220. xml.target!
  221. end
  222. def add_credentials(xml)
  223. xml.tag! 'RequesterCredentials', CREDENTIALS_NAMESPACES do
  224. xml.tag! 'n1:Credentials' do
  225. xml.tag! 'Username', @options[:login]
  226. xml.tag! 'Password', @options[:password]
  227. xml.tag! 'Subject', @options[:subject]
  228. xml.tag! 'Signature', @options[:signature] unless @options[:signature].blank?
  229. end
  230. end
  231. end
  232. def add_address(xml, element, address)
  233. return if address.nil?
  234. xml.tag! element do
  235. xml.tag! 'n2:Name', address[:name]
  236. xml.tag! 'n2:Street1', address[:address1]
  237. xml.tag! 'n2:Street2', address[:address2]
  238. xml.tag! 'n2:CityName', address[:city]
  239. xml.tag! 'n2:StateOrProvince', address[:state].blank? ? 'N/A' : address[:state]
  240. xml.tag! 'n2:Country', address[:country]
  241. xml.tag! 'n2:PostalCode', address[:zip]
  242. xml.tag! 'n2:Phone', address[:phone]
  243. end
  244. end
  245. def add_payment_detail_item(xml, item)
  246. currency_code = options[:currency] || currency(item[:amount])
  247. xml.tag! 'n2:PaymentDetailItem' do
  248. xml.tag! 'n2:Name', item[:name] unless item[:name].blank?
  249. xml.tag! 'n2:Description', item[:description] unless item[:description].blank?
  250. xml.tag! 'n2:Number', item[:sku] unless item[:sku].blank?
  251. xml.tag! 'n2:Quantity', item[:qty] unless item[:qty].blank?
  252. xml.tag! 'n2:Amount', amount(item[:amount]), 'currencyID' => currency_code unless item[:amount].blank?
  253. xml.tag! 'n2:Tax', amount(item[:tax]), 'currencyID' => currency_code unless item[:tax].blank?
  254. xml.tag! 'n2:ItemWeight', item[:weight] unless item[:weight].blank?
  255. xml.tag! 'n2:ItemHeight', item[:height] unless item[:height].blank?
  256. xml.tag! 'n2:ItemWidth', item[:width] unless item[:width].blank?
  257. xml.tag! 'n2:ItemLength', item[:length] unless item[:length].blank?
  258. # not doing this yet TODO
  259. # xml.tag! 'n2:EbayItemPaymentDetailsItem', item[:name]
  260. end
  261. end
  262. def add_payment_details(xml, money, options)
  263. currency_code = options[:currency] || currency(money)
  264. # COULD USE options[:currency] || currency(options[:actual_opt])
  265. xml.tag! 'n2:PaymentDetails' do
  266. xml.tag! 'n2:OrderTotal', amount(money), 'currencyID' => currency_code
  267. # All of the values must be included together and add up to the order total
  268. if [:subtotal, :shipping, :handling, :tax].all?{ |o| options.has_key?(o) }
  269. xml.tag! 'n2:ItemTotal', amount(options[:subtotal]), 'currencyID' => currency_code
  270. xml.tag! 'n2:ShippingTotal', amount(options[:shipping]),'currencyID' => currency_code
  271. xml.tag! 'n2:HandlingTotal', amount(options[:handling]),'currencyID' => currency_code
  272. xml.tag! 'n2:TaxTotal', amount(options[:tax]), 'currencyID' => currency_code
  273. end
  274. # don't enforce inclusion yet - see how it works
  275. xml.tag! 'n2:InsuranceOptionOffered', options[:insurance_offered] ? '1' : '0'
  276. xml.tag! 'n2:InsuranceTotal', amount(options[:insurance]), 'currencyID' => currency_code unless options[:insurance].blank?
  277. xml.tag! 'n2:ShippingDiscount', amount(options[:ship_discount]), 'currencyID' => currency_code unless options[:ship_discount].blank?
  278. # query - use slices too? or just risk reject? (QQ: injection risk???)
  279. xml.tag! 'n2:OrderDescription', options[:description] unless options[:description].blank?
  280. xml.tag! 'n2:Custom', options[:custom] unless options[:custom].blank?
  281. xml.tag! 'n2:InvoiceID', options[:order_id] unless options[:order_id].blank?
  282. xml.tag! 'n2:ButtonSource', application_id.to_s.slice(0,32) unless application_id.blank?
  283. xml.tag! 'n2:NotifyURL', options[:notify_url] unless options[:notify_url].blank?
  284. add_address(xml, 'n2:ShipToAddress', options[:shipping_address] || options[:address])
  285. options[:items].each {|i| add_payment_detail_item xml, i } if options[:items]
  286. end
  287. end
  288. def endpoint_url
  289. URLS[test? ? :test : :live][@options[:signature].blank? ? :certificate : :signature]
  290. end
  291. def commit(action, request)
  292. response = parse(action, ssl_post(endpoint_url, build_request(request)))
  293. File.open("/tmp/paypal", "a") do |f|
  294. f.puts "\n\n\n ************** #{Time.now}\n"
  295. f.puts endpoint_url.inspect
  296. f.puts "\n\n"
  297. f.puts request.to_yaml
  298. f.puts "\n\n"
  299. f.puts response.to_yaml
  300. f.puts "\n\n"
  301. end
  302. build_response(successful?(response), message_from(response), response,
  303. :test => test?,
  304. :authorization => authorization_from(response),
  305. :fraud_review => fraud_review?(response),
  306. :avs_result => { :code => response[:avs_code] },
  307. :cvv_result => response[:cvv2_code]
  308. )
  309. end
  310. def fraud_review?(response)
  311. response[:error_codes] == FRAUD_REVIEW_CODE
  312. end
  313. def authorization_from(response)
  314. response[:transaction_id] || response[:authorization_id] || response[:refund_transaction_id] # middle one is from reauthorization
  315. end
  316. def successful?(response)
  317. SUCCESS_CODES.include?(response[:ack])
  318. end
  319. def message_from(response)
  320. response[:message] || response[:ack]
  321. end
  322. end
  323. end
  324. end