lib/active_merchant/billing/gateways/balanced.rb in activemerchant-1.43.3 vs lib/active_merchant/billing/gateways/balanced.rb in activemerchant-1.44.0

- old
+ new

@@ -1,10 +1,9 @@ require 'json' module ActiveMerchant #:nodoc: module Billing #:nodoc: - # For more information on Balanced visit https://www.balancedpayments.com # or visit #balanced on irc.freenode.net # # Instantiate a instance of BalancedGateway by passing through your # Balanced API key secret. @@ -15,451 +14,211 @@ # 2. Click "Get started" # 3. The next screen will give you a test API key of your own # 4. When you're ready to generate a production API key click the "Go # live" button on the Balanced dashboard and fill in your marketplace # details. - # - # ==== Overview - # - # Balanced provides a RESTful API, all entities within Balanced are - # represented by their respective URIs, these are returned in the - # `authorization` parameter of the Active Merchant Response object. - # - # All Response objects will contain a hash property called `params` which - # holds the raw JSON dictionary returned by Balanced. You can find - # properties about the operation performed and the object that represents - # it within this hash. - # - # All operations within Balanced are tied to an account, as such, when you - # perform an `authorization` or a `capture` with a new credit card you - # must ensure you also pass the `:email` property within the `options` - # parameter. - # - # For more details about Balanced's API visit: - # https://www.balancedpayments.com/docs - # - # ==== Terminology & Transaction Flow - # - # * An `authorization` operation will return a Hold URI. An `authorization` - # within Balanced is valid until the `expires_at` property. You can see the - # exact date of the expiry on the Response object by inspecting the - # property `response.params['expires_at']`. The resulting Hold may be - # `capture`d or `void`ed at any time before the `expires_at` date for - # any amount up to the full amount of the original `authorization`. - # * A `capture` operation will return a Debit URI. You must pass the URI of - # the previously performed `authorization` - # * A `purchase` will create a Hold and Debit in a single operation and - # return the URI of the resulting Debit. - # * A `void` operation must be performed on an existing `authorization` - # and will result in releasing the funds reserved by the - # `authorization`. - # * The `refund` operation must be performed on a previously captured - # Debit URI. You may refund any fraction of the original amount of the - # debit up to the original total. - # class BalancedGateway < Gateway - VERSION = '1.0.0' + VERSION = "2.0.0" - TEST_URL = LIVE_URL = 'https://api.balancedpayments.com' + self.live_url = 'https://api.balancedpayments.com' - # The countries the gateway supports merchants from as 2 digit ISO - # country codes self.supported_countries = ['US'] self.supported_cardtypes = [:visa, :master, :american_express, :discover] self.homepage_url = 'https://www.balancedpayments.com/' self.display_name = 'Balanced' self.money_format = :cents - class Error < ActiveMerchant::ActiveMerchantError - attr_reader :response - - def initialize(response, msg=nil) - @response = response - super(msg || response['description']) - end - end - - class CardDeclined < Error - end - # Creates a new BalancedGateway # - # The gateway requires that a valid api_key be passed in the +options+ - # hash. - # # ==== Options # # * <tt>:login</tt> -- The Balanced API Secret (REQUIRED) def initialize(options = {}) requires!(options, :login) super - initialize_marketplace(options[:marketplace] || load_marketplace) end - # Performs an authorization (Hold in Balanced nonclementure), which - # reserves the funds on the customer's credit card, but does not charge - # the card. An authorization is valid until the `expires_at` field in - # the params Hash passes. See `response.params['expires_at']`. The exact - # amount of time until an authorization expires depends on the card - # issuer. - # - # If you pass a previously tokenized `credit_card` URI the only other - # parameter required is `money`. If you pass `credit_card` as a hash of - # credit card information you must also pass `options` with a `:email` - # entry. - # - # ==== Parameters - # - # * <tt>money</tt> -- The amount to be authorized as an Integer value in cents. - # * <tt>credit_card</tt> -- A hash of credit card details for this - # transaction or the URI of a card previously stored in Balanced. - # * <tt>options</tt> -- A hash of optional parameters. - # - # ==== Options - # - # If you are passing a new credit card you must pass one of these two - # parameters - # - # * <tt>email</tt> -- the email address of user associated with this - # purchase. - # * <tt>account_uri</tt> -- `account_uri` is the URI of an existing - # Balanced account. - def authorize(money, credit_card, options = {}) - if credit_card.respond_to?(:number) - requires!(options, :email) unless options[:account_uri] - end - + def purchase(money, payment_method, options = {}) post = {} - post[:amount] = money + add_amount(post, money) post[:description] = options[:description] add_common_params(post, options) - create_or_find_account(post, options) - add_credit_card(post, credit_card, options) - add_address(credit_card, options) - - create_transaction(:post, @holds_uri, post) - rescue Error => ex - failed_response(ex.response) + MultiResponse.run do |r| + identifier = if(payment_method.respond_to?(:number)) + r.process{store(payment_method, options)} + r.authorization + else + payment_method + end + r.process{commit("debits", "cards/#{card_identifier_from(identifier)}/debits", post)} + end end - # Perform a purchase, which is an authorization and capture in a single - # operation. - # - # ==== Parameters - # - # * <tt>money</tt> -- The amount to be purchased as an Integer value in cents. - # * <tt>credit_card</tt> -- A hash of credit card details for this - # transaction or the URI of a card previously stored in Balanced. - # * <tt>options</tt> -- A hash of optional parameters. - # - # ==== Options - # - # If you are passing a new credit card you must pass one of these two - # parameters - # - # * <tt>email</tt> -- the email address of user associated with this - # purchase. - # * <tt>account_uri</tt> -- `account_uri` is the URI of an existing - # Balanced account. - # - # If you are passing a new card URI from balanced.js, you should pass - # the customer's name - # - # * <tt>name</tt> -- the customer's name, to appear on the Account - # on Balanced. - def purchase(money, credit_card, options = {}) - if credit_card.respond_to?('number') - requires!(options, :email) unless options[:account_uri] - end - + def authorize(money, payment_method, options = {}) post = {} - post[:amount] = money + add_amount(post, money) post[:description] = options[:description] add_common_params(post, options) - create_or_find_account(post, options) - add_credit_card(post, credit_card, options) - add_address(credit_card, options) - - create_transaction(:post, @debits_uri, post) - rescue Error => ex - failed_response(ex.response) + MultiResponse.run do |r| + identifier = if(payment_method.respond_to?(:number)) + r.process{store(payment_method, options)} + r.authorization + else + payment_method + end + r.process{commit("card_holds", "cards/#{card_identifier_from(identifier)}/card_holds", post)} + end end - # Captures the funds from an authorized transaction (Hold). - # - # ==== Parameters - # - # * <tt>money</tt> -- The amount to be captured as an Integer value in - # cents. If omitted the full amount of the original authorization - # transaction will be captured. - # * <tt>authorization</tt> -- The uri of an authorization returned from - # an authorize request. - # - # ==== Options - # - # * <tt>description</tt> -- A string that will be displayed on the - # Balanced dashboard - def capture(money, authorization, options = {}) + def capture(money, identifier, options = {}) post = {} - post[:hold_uri] = authorization - post[:amount] = money if money + add_amount(post, money) post[:description] = options[:description] if options[:description] add_common_params(post, options) - create_transaction(:post, @debits_uri, post) - rescue Error => ex - failed_response(ex.response) + commit("debits", "card_holds/#{reference_identifier_from(identifier)}/debits", post) end - # Void a previous authorization (Hold) - # - # ==== Parameters - # - # * <tt>authorization</tt> -- The uri of the authorization returned from - # an `authorize` request. - def void(authorization, options = {}) + def void(identifier, options = {}) post = {} post[:is_void] = true add_common_params(post, options) - create_transaction(:put, authorization, post) - rescue Error => ex - failed_response(ex.response) + commit("card_holds", "card_holds/#{reference_identifier_from(identifier)}", post, :put) end - # Refund a transaction. - # - # Returns the money debited from a card to the card from the - # marketplace's escrow balance. - # - # ==== Parameters - # - # * <tt>debit_uri</tt> -- The uri of the original transaction against - # which the refund is being issued. - # * <tt>options</tt> -- A hash of parameters. - # - # ==== Options - # - # * <tt>`:amount`<tt> -- specify an amount if you want to perform a - # partial refund. This value will default to the total amount of the - # debit that has not been refunded so far. - def refund(amount, debit_uri = "deprecated", options = {}) - if(debit_uri == "deprecated" || debit_uri.kind_of?(Hash)) - deprecated "Calling the refund method without an amount parameter is deprecated and will be removed in a future version." - return refund(options[:amount], amount, options) - end - - requires!(debit_uri) + def refund(money, identifier, options = {}) post = {} - post[:debit_uri] = debit_uri - post[:amount] = amount + add_amount(post, money) post[:description] = options[:description] add_common_params(post, options) - create_transaction(:post, @refunds_uri, post) - rescue Error => ex - failed_response(ex.response) + + commit("refunds", "debits/#{reference_identifier_from(identifier)}/refunds", post) end - # Stores a card and email address - # - # ==== Parameters - # - # * <tt>credit_card</tt> -- - def store(credit_card, options = {}) - requires!(options, :email) + def store(credit_card, options={}) post = {} - account_uri = create_or_find_account(post, options) - if credit_card.respond_to? :number - card_uri = add_credit_card(post, credit_card, options) - else - card_uri = associate_card_to_account(account_uri, credit_card) - end - is_test = false - if @marketplace_uri - is_test = (@marketplace_uri.index("TEST") ? true : false) - end + post[:number] = credit_card.number + post[:expiration_month] = credit_card.month + post[:expiration_year] = credit_card.year + post[:cvv] = credit_card.verification_value if credit_card.verification_value? + post[:name] = credit_card.name if credit_card.name - Response.new(true, "Card stored", {}, :test => is_test, :authorization => [card_uri, account_uri].compact.join(';')) - rescue Error => ex - failed_response(ex.response) + add_address(post, options) + + commit("cards", "cards", post) end private - # Load URIs for this marketplace by inspecting the marketplace object - # returned from the uri. http://en.wikipedia.org/wiki/HATEOAS - def load_marketplace - response = http_request(:get, '/v1/marketplaces') - if error?(response) - raise Error.new(response, 'Invalid login credentials supplied') + def reference_identifier_from(identifier) + case identifier + when %r{\|} + uri = identifier. + split("|"). + detect{|part| part.size > 0} + uri.split("/")[2] + when %r{\/} + identifier.split("/")[5] + else + identifier end - response['items'][0] end - def initialize_marketplace(marketplace) - @marketplace_uri = marketplace['uri'] - @holds_uri = marketplace['holds_uri'] - @debits_uri = marketplace['debits_uri'] - @cards_uri = marketplace['cards_uri'] - @accounts_uri = marketplace['accounts_uri'] - @refunds_uri = marketplace['refunds_uri'] + def card_identifier_from(identifier) + identifier.split("/").last end - def create_or_find_account(post, options) - account_uri = nil - - if options.has_key? :account_uri - account_uri = options[:account_uri] - end - - if account_uri == nil - post[:name] = options[:name] if options[:name] - post[:email_address] = options[:email] - post[:meta] = options[:meta] if options[:meta] - - # create an account - response = http_request(:post, @accounts_uri, post) - - if response.has_key? 'uri' - account_uri = response['uri'] - elsif error?(response) - # lookup account from Balanced, account_uri should be in the - # exception in a dictionary called extras - account_uri = response['extras']['account_uri'] - raise Error.new(response) unless account_uri - end - end - - post[:account_uri] = account_uri - - account_uri + def add_amount(post, money) + post[:amount] = amount(money) if money end - def add_address(credit_card, options) - return unless credit_card.kind_of?(Hash) - if address = options[:billing_address] || options[:address] - credit_card[:street_address] = address[:address1] if address[:address1] - credit_card[:street_address] += ' ' + address[:address2] if address[:address2] - credit_card[:postal_code] = address[:zip] if address[:zip] - credit_card[:country] = address[:country] if address[:country] + def add_address(post, options) + address = (options[:billing_address] || options[:address]) + if(address && address[:zip].present?) + post[:address] = {} + post[:address][:line1] = address[:address1] if address[:address1] + post[:address][:line2] = address[:address2] if address[:address2] + post[:address][:city] = address[:city] if address[:city] + post[:address][:state] = address[:state] if address[:state] + post[:address][:postal_code] = address[:zip] if address[:zip] + post[:address][:country_code] = address[:country] if address[:country] end end def add_common_params(post, options) - common_params = [ - :appears_on_statement_as, - :on_behalf_of_uri, - :meta - ] - post.update(options.select{|key, _| common_params.include?(key)}) + post[:appears_on_statement_as] = options[:appears_on_statement_as] + post[:on_behalf_of_uri] = options[:on_behalf_of_uri] + post[:meta] = options[:meta] end - def add_credit_card(post, credit_card, options) - if credit_card.respond_to? :number - card = {} - card[:card_number] = credit_card.number - card[:expiration_month] = credit_card.month - card[:expiration_year] = credit_card.year - card[:security_code] = credit_card.verification_value if credit_card.verification_value? - card[:name] = credit_card.name if credit_card.name - - add_address(card, options) - - response = http_request(:post, @cards_uri, card) - if error?(response) - raise CardDeclined, response - end - card_uri = response['uri'] - - associate_card_to_account(post[:account_uri], card_uri) - - post[:card_uri] = card_uri - elsif credit_card.kind_of?(String) - associate_card_to_account(post[:account_uri], credit_card) unless options[:account_uri] - post[:card_uri] = credit_card - end - - post[:card_uri] - end - - def associate_card_to_account(account_uri, card_uri) - http_request(:put, account_uri, :card_uri => card_uri) - end - - def http_request(method, url, parameters={}, meta={}) - begin - if method == :get - raw_response = ssl_get(LIVE_URL + url, headers(meta)) - else - raw_response = ssl_request(method, - LIVE_URL + url, - post_data(parameters), - headers(meta)) - end - parse(raw_response) + def commit(entity_name, path, post, method=:post) + raw_response = begin + parse(ssl_request( + method, + live_url + "/#{path}", + post_data(post), + headers + )) rescue ResponseError => e - raw_response = e.response.body - response_error(raw_response) - rescue JSON::ParserError - json_error(raw_response) + raise unless(e.response.code.to_s =~ /4\d\d/) + parse(e.response.body) end - end - def create_transaction(method, url, parameters, meta={}) - response = http_request(method, url, parameters, meta) - success = !error?(response) - - Response.new(success, - (success ? "Transaction approved" : response["description"]), - response, - :test => (@marketplace_uri.index("TEST") ? true : false), - :authorization => response["uri"] + Response.new( + success_from(entity_name, raw_response), + message_from(raw_response), + raw_response, + authorization: authorization_from(entity_name, raw_response), + test: test?, ) end - def failed_response(response) - is_test = false - if @marketplace_uri - is_test = (@marketplace_uri.index("TEST") ? true : false) + def success_from(entity_name, raw_response) + entity = (raw_response[entity_name] || []).first + if(!entity) + false + elsif((entity_name == "refunds") && entity.include?("status")) + %w(succeeded pending).include?(entity["status"]) + elsif(entity.include?("status")) + (entity["status"] == "succeeded") + elsif(entity_name == "cards") + !!entity["id"] + else + false end - - Response.new(false, - response["description"], - response, - :test => is_test - ) end - def parse(body) - JSON.parse(body) + def message_from(raw_response) + if(raw_response["errors"]) + error = raw_response["errors"].first + (error["additional"] || error["message"] || error["description"]) + else + "Success" + end end - def response_error(raw_response) - begin - parse(raw_response) - rescue JSON::ParserError - json_error(raw_response) - end + def authorization_from(entity_name, raw_response) + entity = (raw_response[entity_name] || []).first + (entity && entity["id"]) end - def json_error(raw_response) - msg = 'Invalid response received from the Balanced API. Please contact support@balancedpayments.com if you continue to receive this message.' - msg += " (The raw response returned by the API was #{raw_response.inspect})" + def parse(body) + JSON.parse(body) + rescue JSON::ParserError + message = 'Invalid response received from the Balanced API. Please contact support@balancedpayments.com if you continue to receive this message.' + message += " (The raw response returned by the API was #{raw_response.inspect})" { - "error" => { - "message" => msg - } + "errors" => [{ + "message" => message + }] } end - def error?(response) - response.key?('status_code') - end - def post_data(params) return nil unless params params.map do |key, value| next if value.blank? @@ -473,22 +232,23 @@ "#{key}=#{CGI.escape(value.to_s)}" end end.compact.join("&") end - def headers(meta={}) - @@ua ||= JSON.dump({ - :bindings_version => ActiveMerchant::VERSION, - :lang => 'ruby', - :lang_version => "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", - :lib_version => BalancedGateway::VERSION, - :platform => RUBY_PLATFORM, - :publisher => 'active_merchant' - }) + def headers + @@ua ||= JSON.dump( + bindings_version: ActiveMerchant::VERSION, + lang: 'ruby', + lang_version: "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})", + lib_version: BalancedGateway::VERSION, + platform: RUBY_PLATFORM, + publisher: 'active_merchant' + ) { "Authorization" => "Basic " + Base64.encode64(@options[:login].to_s + ":").strip, - "User-Agent" => "Balanced/v1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}", + "User-Agent" => "Balanced/v1.1 ActiveMerchantBindings/#{ActiveMerchant::VERSION}", + "Accept" => "application/vnd.api+json;revision=1.1", "X-Balanced-User-Agent" => @@ua, } end end end