lib/active_merchant/billing/gateways/cyber_source.rb in activemerchant-1.21.0 vs lib/active_merchant/billing/gateways/cyber_source.rb in activemerchant-1.22.0

- old
+ new

@@ -1,39 +1,41 @@ module ActiveMerchant #:nodoc: module Billing #:nodoc: # See the remote and mocked unit test files for example usage. Pay special attention to the contents of the options hash. # # Initial setup instructions can be found in http://cybersource.com/support_center/implementation/downloads/soap_api/SOAP_toolkits.pdf - # - # Debugging + # + # Debugging # If you experience an issue with this gateway be sure to examine the transaction information from a general transaction search inside the CyberSource Business - # Center for the full error messages including field names. + # Center for the full error messages including field names. # # Important Notes - # * AVS and CVV only work against the production server. You will always get back X for AVS and no response for CVV against the test server. - # * Nexus is the list of states or provinces where you have a physical presence. Nexus is used to calculate tax. Leave blank to tax everyone. - # * If you want to calculate VAT for overseas customers you must supply a registration number in the options hash as vat_reg_number. + # * AVS and CVV only work against the production server. You will always get back X for AVS and no response for CVV against the test server. + # * Nexus is the list of states or provinces where you have a physical presence. Nexus is used to calculate tax. Leave blank to tax everyone. + # * If you want to calculate VAT for overseas customers you must supply a registration number in the options hash as vat_reg_number. # * productCode is a value in the line_items hash that is used to tell CyberSource what kind of item you are selling. It is used when calculating tax/VAT. # * All transactions use dollar values. class CyberSourceGateway < Gateway TEST_URL = 'https://ics2wstest.ic3.com/commerce/1.x/transactionProcessor' LIVE_URL = 'https://ics2ws.ic3.com/commerce/1.x/transactionProcessor' - + + XSD_VERSION = "1.69" + # visa, master, american_express, discover self.supported_cardtypes = [:visa, :master, :american_express, :discover] self.supported_countries = ['US'] self.default_currency = 'USD' self.homepage_url = 'http://www.cybersource.com' self.display_name = 'CyberSource' - + # map credit card to the CyberSource expected representation @@credit_card_codes = { :visa => '001', :master => '002', :american_express => '003', :discover => '004' - } + } # map response codes to something humans can read @@response_codes = { :r100 => "Successful transaction", :r101 => "Request is missing one or more required fields" , @@ -41,21 +43,21 @@ :r150 => "General failure", :r151 => "The request was received but a server time-out occurred", :r152 => "The request was received, but a service timed out", :r200 => "The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the AVS check", :r201 => "The issuing bank has questions about the request", - :r202 => "Expired card", - :r203 => "General decline of the card", - :r204 => "Insufficient funds in the account", - :r205 => "Stolen or lost card", - :r207 => "Issuing bank unavailable", - :r208 => "Inactive card or card not authorized for card-not-present transactions", - :r209 => "American Express Card Identifiction Digits (CID) did not match", - :r210 => "The card has reached the credit limit", - :r211 => "Invalid card verification number", - :r221 => "The customer matched an entry on the processor's negative file", - :r230 => "The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check", + :r202 => "Expired card", + :r203 => "General decline of the card", + :r204 => "Insufficient funds in the account", + :r205 => "Stolen or lost card", + :r207 => "Issuing bank unavailable", + :r208 => "Inactive card or card not authorized for card-not-present transactions", + :r209 => "American Express Card Identifiction Digits (CID) did not match", + :r210 => "The card has reached the credit limit", + :r211 => "Invalid card verification number", + :r221 => "The customer matched an entry on the processor's negative file", + :r230 => "The authorization request was approved by the issuing bank but declined by CyberSource because it did not pass the card verification check", :r231 => "Invalid account number", :r232 => "The card type is not accepted by the payment processor", :r233 => "General decline by the processor", :r234 => "A problem exists with your CyberSource merchant configuration", :r235 => "The requested amount exceeds the originally authorized amount", @@ -70,48 +72,48 @@ :r244 => "The bank account number failed the validation check", :r246 => "The capture or credit is not voidable because the capture or credit information has already been submitted to your processor", :r247 => "You requested a credit for a capture that was previously voided", :r250 => "The request was received, but a time-out occurred with the payment processor", :r254 => "Your CyberSource account is prohibited from processing stand-alone refunds", - :r255 => "Your CyberSource account is not configured to process the service in the country you specified" + :r255 => "Your CyberSource account is not configured to process the service in the country you specified" } # These are the options that can be used when creating a new CyberSource Gateway object. - # - # :login => your username # - # :password => the transaction key you generated in the Business Center + # :login => your username # + # :password => the transaction key you generated in the Business Center + # # :test => true sets the gateway to test mode # - # :vat_reg_number => your VAT registration number + # :vat_reg_number => your VAT registration number # # :nexus => "WI CA QC" sets the states/provinces where you have a physical presense for tax purposes # - # :ignore_avs => true don't want to use AVS so continue processing even if AVS would have failed + # :ignore_avs => true don't want to use AVS so continue processing even if AVS would have failed # - # :ignore_cvv => true don't want to use CVV so continue processing even if CVV would have failed + # :ignore_cvv => true don't want to use CVV so continue processing even if CVV would have failed def initialize(options = {}) requires!(options, :login, :password) @options = options super - end + end # Should run against the test servers or not? def test? @options[:test] || Base.gateway_mode == :test end - - # Request an authorization for an amount from CyberSource + + # Request an authorization for an amount from CyberSource # - # You must supply an :order_id in the options hash + # You must supply an :order_id in the options hash def authorize(money, creditcard, options = {}) requires!(options, :order_id, :email) setup_address_hash(options) commit(build_auth_request(money, creditcard, options), options ) end - + def auth_reversal(money, identification, options = {}) commit(build_auth_reversal_request(money, identification, options), options) end # Capture an authorization that has previously been requested @@ -119,35 +121,103 @@ setup_address_hash(options) commit(build_capture_request(money, authorization, options), options) end # Purchase is an auth followed by a capture - # You must supply an order_id in the options hash - def purchase(money, creditcard, options = {}) + # You must supply an order_id in the options hash + def purchase(money, payment_source, options = {}) requires!(options, :order_id, :email) setup_address_hash(options) - commit(build_purchase_request(money, creditcard, options), options) + if payment_source.is_a?(String) + requires!(options, [:type, :credit_card, :check]) + commit(build_subscription_purchase_request(money, payment_source, options), options) + else + commit(build_purchase_request(money, payment_source, options), options) + end end - + def void(identification, options = {}) commit(build_void_request(identification, options), options) end def refund(money, identification, options = {}) commit(build_credit_request(money, identification, options), options) end - + def credit(money, identification, options = {}) deprecated CREDIT_DEPRECATION_MESSAGE refund(money, identification, options) end + # Creates or updates a cybersource customer profile, aka a subscription with type "on-demand" + # to charge the card while creating a profile, pass options[:setup_fee] => money + def store(credit_card_or_reference, options = {}) + + requires!(options, :order_id) + setup_address_hash(options) + + if credit_card_or_reference.respond_to?(:number) + # create subscription + requires!(options, :billing_address, :email) + requires!(options[:billing_address], :first_name, :last_name) + + # set subscription options for storing the credit card + options[:subscription] ||={} + options[:subscription].merge!(:frequency => "on-demand", :amount => 0, :auto_renew => false) + + setup_address_hash(options) + request = build_create_subscription_request(credit_card_or_reference, options) + else + # update subscription + request = build_update_subscription_request(credit_card_or_reference, options) + end + + commit(request, options) + end + + # retrieves a customer subscription/profile + def retrieve(reference, options = {}) + requires!(options, :order_id) + commit(build_retrieve_subscription_request(reference, options), options) + end + + # removes a customer subscription/profile + def unstore(reference, options = {}) + requires!(options, :order_id) + commit(build_delete_subscription_request(reference, options), options) + end + + # Creates or updates a Cybersource recurring payment profile/subscription + def recurring(money, credit_card_or_reference, options = {}) + requires!(options, :order_id, :subscription) + requires!(options[:subscription], [:frequency, "on-demand", "weekly", "bi-weekly", "semi-monthly", "quarterly", "quad-weekly", "semi-annually", "annually"]) + + options[:subscription].merge!(:amount => money) + + setup_address_hash(options) + + if credit_card_or_reference.respond_to?(:number) + # create subscription + requires!(options, :billing_address, :email) + requires!(options[:billing_address], :first_name, :last_name) + + setup_address_hash(options) + request = build_create_subscription_request(credit_card_or_reference, options) + else + # update subscription + request = build_update_subscription_request(credit_card_or_reference, options) + end + + commit(request, options) + end + + # CyberSource requires that you provide line item information for tax calculations # If you do not have prices for each item or want to simplify the situation then pass in one fake line item that costs the subtotal of the order # - # The line_item hash goes in the options hash and should look like - # + # The line_item hash goes in the options hash and should look like + # # :line_items => [ # { # :declared_value => '1', # :quantity => '2', # :code => 'default', @@ -163,69 +233,70 @@ # } # ] # # This functionality is only supported by this particular gateway may # be changed at any time - def calculate_tax(creditcard, options) + def calculate_tax(options) requires!(options, :line_items) + setup_address_hash(options) - commit(build_tax_calculation_request(creditcard, options), options) + commit(build_tax_calculation_request(options), options) end - - private - # Create all address hash key value pairs so that we still function if we were only provided with one or two of them + + private + # Create all address hash key value pairs so that we still function if we were only provided with one or two of them def setup_address_hash(options) options[:billing_address] = options[:billing_address] || options[:address] || {} options[:shipping_address] = options[:shipping_address] || {} end - + def build_auth_request(money, creditcard, options) xml = Builder::XmlMarkup.new :indent => 2 - add_address(xml, creditcard, options[:billing_address], options) + add_address(xml, options[:billing_address], options) add_purchase_data(xml, money, true, options) add_creditcard(xml, creditcard) add_auth_service(xml) add_business_rules_data(xml) xml.target! end - def build_tax_calculation_request(creditcard, options) + def build_tax_calculation_request(options) xml = Builder::XmlMarkup.new :indent => 2 - add_address(xml, creditcard, options[:billing_address], options, false) - add_address(xml, creditcard, options[:shipping_address], options, true) + add_address(xml, options[:billing_address], options, false) + add_address(xml, options[:shipping_address], options, true) unless options[:shipping_address].empty? add_line_item_data(xml, options) add_purchase_data(xml, 0, false, options) - add_tax_service(xml) + add_tax_service(xml, options) add_business_rules_data(xml) xml.target! end - + def build_capture_request(money, authorization, options) order_id, request_id, request_token = authorization.split(";") options[:order_id] = order_id xml = Builder::XmlMarkup.new :indent => 2 add_purchase_data(xml, money, true, options) add_capture_service(xml, request_id, request_token) add_business_rules_data(xml) xml.target! - end + end - def build_purchase_request(money, creditcard, options) + def build_purchase_request(money, payment_source, options) xml = Builder::XmlMarkup.new :indent => 2 - add_address(xml, creditcard, options[:billing_address], options) + add_address(xml, options[:billing_address], options) add_purchase_data(xml, money, true, options) - add_creditcard(xml, creditcard) - add_purchase_service(xml, options) + add_payment_source(xml, payment_source) + add_purchase_service(xml, payment_source, options) add_business_rules_data(xml) xml.target! end - + def build_void_request(identification, options) order_id, request_id, request_token = identification.split(";") options[:order_id] = order_id - + xml = Builder::XmlMarkup.new :indent => 2 add_void_service(xml, request_id, request_token) xml.target! end @@ -239,99 +310,213 @@ end def build_credit_request(money, identification, options) order_id, request_id, request_token = identification.split(";") options[:order_id] = order_id - + xml = Builder::XmlMarkup.new :indent => 2 add_purchase_data(xml, money, true, options) add_credit_service(xml, request_id, request_token) - + xml.target! end + def build_create_subscription_request(payment_source, options) + xml = Builder::XmlMarkup.new :indent => 2 + add_address(xml, options[:billing_address], options) + add_purchase_data(xml, options[:setup_fee], true, options) + + + case determine_funding_source(payment_source) + when :credit_card then add_creditcard(xml, payment_source) + when :check then add_check(xml, payment_source) + else raise ArgumentError, "Unsupported funding source provided" + end + + add_subscription(xml, options, payment_source) + add_subscription_create_service(xml, options) + add_business_rules_data(xml) + xml.target! + end + + def build_update_subscription_request(identification, options) + reference_code, subscription_id, request_token = identification.split(";") + options[:subscription] ||= {} + options[:subscription][:subscription_id] = subscription_id + + xml = Builder::XmlMarkup.new :indent => 2 + add_address(xml, options[:billing_address], options) unless options[:billing_address].blank? + add_purchase_data(xml, options[:setup_fee], true, options) unless options[:setup_fee].blank? + add_creditcard(xml, options[:credit_card]) if options[:credit_card] + add_subscription(xml, options) + add_subscription_update_service(xml, options) + add_business_rules_data(xml) + xml.target! + end + + def build_retrieve_subscription_request(identification, options) + reference_code, subscription_id, request_token = identification.split(";") + options[:subscription] ||= {} + options[:subscription][:subscription_id] = subscription_id + + xml = Builder::XmlMarkup.new :indent => 2 + add_subscription(xml, options) + add_subscription_retrieve_service(xml, options) + xml.target! + end + + def build_delete_subscription_request(identification, options) + reference_code, subscription_id, request_token = identification.split(";") + options[:subscription] ||= {} + options[:subscription][:subscription_id] = subscription_id + + xml = Builder::XmlMarkup.new :indent => 2 + add_subscription(xml, options) + add_subscription_delete_service(xml, options) + xml.target! + end + + def build_subscription_purchase_request(money, identification, options) + reference_code, subscription_id, request_token = identification.split(";") + options[:subscription] ||= {} + options[:subscription][:subscription_id] = subscription_id + + xml = Builder::XmlMarkup.new :indent => 2 + add_purchase_data(xml, money, true, options) + add_subscription(xml, options) + + case options[:type] + when :credit_card then add_cc_purchase_service(xml, options) + when :check then add_check_service(xml) + end + + add_business_rules_data(xml) + xml.target! + end + def add_business_rules_data(xml) xml.tag! 'businessRules' do xml.tag!('ignoreAVSResult', 'true') if @options[:ignore_avs] xml.tag!('ignoreCVResult', 'true') if @options[:ignore_cvv] - end + end end - + def add_line_item_data(xml, options) options[:line_items].each_with_index do |value, index| xml.tag! 'item', {'id' => index} do - xml.tag! 'unitPrice', amount(value[:declared_value]) + xml.tag! 'unitPrice', amount(value[:declared_value]) xml.tag! 'quantity', value[:quantity] xml.tag! 'productCode', value[:code] || 'shipping_only' xml.tag! 'productName', value[:description] xml.tag! 'productSKU', value[:sku] end end end - + def add_merchant_data(xml, options) xml.tag! 'merchantID', @options[:login] xml.tag! 'merchantReferenceCode', options[:order_id] xml.tag! 'clientLibrary' ,'Ruby Active Merchant' - xml.tag! 'clientLibraryVersion', '1.0' - xml.tag! 'clientEnvironment' , 'Linux' + xml.tag! 'clientLibraryVersion', VERSION + xml.tag! 'clientEnvironment' , RUBY_PLATFORM end - def add_purchase_data(xml, money = 0, include_grand_total = false, options={}) + def add_payment_source(xml, source, options={}) + case determine_funding_source(source) + #when :subscription then add_customer_vault_id(params, source) + when :credit_card then add_creditcard(xml, source) + when :check then add_check(xml, source) + end + end + + def add_purchase_data(xml, money, include_grand_total = false, options={}) + money ||=0 xml.tag! 'purchaseTotals' do xml.tag! 'currency', options[:currency] || currency(money) - xml.tag!('grandTotalAmount', amount(money)) if include_grand_total + xml.tag!('grandTotalAmount', amount(money)) if include_grand_total end end - def add_address(xml, creditcard, address, options, shipTo = false) + def add_address(xml, address, options, shipTo = false) xml.tag! shipTo ? 'shipTo' : 'billTo' do - xml.tag! 'firstName', creditcard.first_name - xml.tag! 'lastName', creditcard.last_name - xml.tag! 'street1', address[:address1] - xml.tag! 'street2', address[:address2] - xml.tag! 'city', address[:city] - xml.tag! 'state', address[:state] - xml.tag! 'postalCode', address[:zip] - xml.tag! 'country', address[:country] - xml.tag! 'email', options[:email] - end + xml.tag! 'firstName', address[:first_name] + xml.tag! 'lastName', address[:last_name] + xml.tag! 'street1', address[:address1] + xml.tag! 'street2', address[:address2] + xml.tag! 'city', address[:city] + xml.tag! 'state', address[:state] + xml.tag! 'postalCode', address[:zip] + xml.tag! 'country', address[:country] + xml.tag! 'company', address[:company] unless address[:company].blank? + xml.tag! 'companyTaxID', address[:companyTaxID] unless address[:company_tax_id].blank? + xml.tag! 'phoneNumber', address[:phone_number] unless address[:phone_number].blank? + xml.tag! 'email', options[:email] unless options[:email].blank? + xml.tag! 'driversLicenseNumber', options[:drivers_license_number] unless options[:drivers_license_number].blank? + xml.tag! 'driversLicenseState', options[:drivers_license_state] unless options[:drivers_license_state].blank? + end end - def add_creditcard(xml, creditcard) + def add_creditcard(xml, creditcard) xml.tag! 'card' do xml.tag! 'accountNumber', creditcard.number xml.tag! 'expirationMonth', format(creditcard.month, :two_digits) xml.tag! 'expirationYear', format(creditcard.year, :four_digits) xml.tag!('cvNumber', creditcard.verification_value) unless (@options[:ignore_cvv] || creditcard.verification_value.blank? ) xml.tag! 'cardType', @@credit_card_codes[card_brand(creditcard).to_sym] end end - def add_tax_service(xml) + def add_check(xml, check) + #convert check object account type into cybs account type code + if check.account_type == "checking" + accountType = check.account_holder_type == "business" ? 'X' : 'C' + else + accountType = 'S' + end + + xml.tag! 'check' do + xml.tag! 'accountNumber', check.account_number + xml.tag! 'accountType', accountType + xml.tag! 'bankTransitNumber', check.routing_number + xml.tag! 'checkNumber', check.number if check.number + end + end + + def add_check_service(xml) + xml.tag! 'ecDebitService', {'run' => 'true'} + end + + def add_tax_service(xml, options) xml.tag! 'taxService', {'run' => 'true'} do - xml.tag!('nexus', @options[:nexus]) unless @options[:nexus].blank? - xml.tag!('sellerRegistration', @options[:vat_reg_number]) unless @options[:vat_reg_number].blank? + xml.tag!('nexus', options[:nexus]) unless options[:nexus].blank? + xml.tag!('sellerRegistration', @ptions[:vat_reg_number]) unless options[:vat_reg_number].blank? end end def add_auth_service(xml) - xml.tag! 'ccAuthService', {'run' => 'true'} + xml.tag! 'ccAuthService', {'run' => 'true'} end def add_capture_service(xml, request_id, request_token) xml.tag! 'ccCaptureService', {'run' => 'true'} do xml.tag! 'authRequestID', request_id xml.tag! 'authRequestToken', request_token end end - def add_purchase_service(xml, options) + def add_purchase_service(xml, source, options) + case determine_funding_source(source) + when :credit_card then add_cc_purchase_service(xml, options) + when :check then add_check_service(xml) + end + end + + def add_cc_purchase_service(xml, options) xml.tag! 'ccAuthService', {'run' => 'true'} xml.tag! 'ccCaptureService', {'run' => 'true'} end - + def add_void_service(xml, request_id, request_token) xml.tag! 'voidService', {'run' => 'true'} do xml.tag! 'voidRequestID', request_id xml.tag! 'voidRequestToken', request_token end @@ -349,11 +534,51 @@ xml.tag! 'captureRequestID', request_id xml.tag! 'captureRequestToken', request_token end end - + + def add_subscription_create_service(xml, options) + add_cc_purchase_service(xml, options) if options[:setup_fee] + xml.tag! 'paySubscriptionCreateService', {'run' => 'true'} + end + + def add_subscription_update_service(xml, options) + add_cc_purchase_service(xml, options) if options[:setup_fee] + xml.tag! 'paySubscriptionUpdateService', {'run' => 'true'} + end + + def add_subscription_retrieve_service(xml, options) + xml.tag! 'paySubscriptionRetrieveService', {'run' => 'true'} + end + + def add_subscription_delete_service(xml, options) + xml.tag! 'paySubscriptionDeleteService', {'run' => 'true'} + end + + def add_subscription(xml, options, payment_source=nil) + if payment_source + xml.tag! 'subscription' do + xml.tag! 'paymentMethod', determine_funding_source(payment_source).to_s.gsub(/_/, " ") + end + end + + xml.tag! 'recurringSubscriptionInfo' do + xml.tag! 'subscriptionID', options[:subscription][:subscription_id] + xml.tag! 'status', options[:subscription][:status] if options[:subscription][:status] + xml.tag! 'amount', options[:subscription][:amount] if options[:subscription][:amount] + xml.tag! 'numberOfPayments', options[:subscription][:occurrences] if options[:subscription][:occurrences] + xml.tag! 'automaticRenew', options[:subscription][:auto_renew] if options[:subscription][:auto_renew] + xml.tag! 'frequency', options[:subscription][:frequency] if options[:subscription][:frequency] + xml.tag! 'startDate', options[:subscription][:start_date].strftime("%Y%m%d") if options[:subscription][:start_date] + xml.tag! 'endDate', options[:subscription][:end_date].strftime("%Y%m%d") if options[:subscription][:end_date] + xml.tag! 'approvalRequired', options[:subscription][:approval_required] || false + xml.tag! 'event', options[:subscription][:event] if options[:subscription][:event] + xml.tag! 'billPayment', options[:subscription][:bill_payment] if options[:subscription][:bill_payment] + end + end + # Where we actually build the full SOAP request using builder def build_request(body, options) xml = Builder::XmlMarkup.new :indent => 2 xml.instruct! xml.tag! 's:Envelope', {'xmlns:s' => 'http://schemas.xmlsoap.org/soap/envelope/'} do @@ -364,67 +589,78 @@ xml.tag! 'wsse:Password', @options[:password], 'Type' => 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText' end end end xml.tag! 's:Body', {'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', 'xmlns:xsd' => 'http://www.w3.org/2001/XMLSchema'} do - xml.tag! 'requestMessage', {'xmlns' => 'urn:schemas-cybersource-com:transaction-data-1.32'} do + xml.tag! 'requestMessage', {'xmlns' => "urn:schemas-cybersource-com:transaction-data-#{XSD_VERSION}"} do add_merchant_data(xml, options) xml << body end end end - xml.target! + xml.target! end - + # Contact CyberSource, make the SOAP request, and parse the reply into a Response object def commit(request, options) - response = parse(ssl_post(test? ? TEST_URL : LIVE_URL, build_request(request, options))) - + request = build_request(request, options) + post_response = ssl_post(test? ? TEST_URL : LIVE_URL, request) + response = parse(post_response) success = response[:decision] == "ACCEPT" - message = @@response_codes[('r' + response[:reasonCode]).to_sym] rescue response[:message] + message = @@response_codes[('r' + response[:reasonCode]).to_sym] rescue response[:message] authorization = success ? [ options[:order_id], response[:requestID], response[:requestToken] ].compact.join(";") : nil - - Response.new(success, message, response, - :test => test?, + + Response.new(success, message, response, + :test => test?, :authorization => authorization, :avs_result => { :code => response[:avsCode] }, :cvv_result => response[:cvCode] ) end - + # Parse the SOAP response # Technique inspired by the Paypal Gateway def parse(xml) reply = {} xml = REXML::Document.new(xml) if root = REXML::XPath.first(xml, "//c:replyMessage") root.elements.to_a.each do |node| - case node.name + case node.name when 'c:reasonCode' reply[:message] = reply(node.text) else parse_element(reply, node) end end - elsif root = REXML::XPath.first(xml, "//soap:Fault") + elsif root = REXML::XPath.first(xml, "//soap:Fault") parse_element(reply, root) reply[:message] = "#{reply[:faultcode]}: #{reply[:faultstring]}" end return reply - end + end def parse_element(reply, node) if node.has_elements? node.elements.each{|e| parse_element(reply, e) } else if node.parent.name =~ /item/ parent = node.parent.name + (node.parent.attributes["id"] ? "_" + node.parent.attributes["id"] : '') reply[(parent + '_' + node.name).to_sym] = node.text - else + else reply[node.name.to_sym] = node.text end end return reply end - end - end -end + + def determine_funding_source(source) + case + when source.is_a?(String) then :subscription + when CreditCard.card_companies.keys.include?(card_brand(source)) then :credit_card + when card_brand(source) == 'check' then :check + else raise ArgumentError, "Unsupported funding source provided" + end + end + + end + end +end