require 'active_merchant/billing/rails' module ActiveMerchant #:nodoc: module Billing #:nodoc: class OmiseGateway < Gateway API_URL = 'https://api.omise.co/' VAULT_URL = 'https://vault.omise.co/' STANDARD_ERROR_CODE_MAPPING = { 'invalid_security_code' => STANDARD_ERROR_CODE[:invalid_cvc], 'failed_capture' => STANDARD_ERROR_CODE[:card_declined] } self.live_url = self.test_url = API_URL # Currency supported by Omise # * Thai Baht with Satang, 50000 (THB500.00) # * Japanese Yen, 500 (JPY500) self.default_currency = 'THB' self.money_format = :cents # Country supported by Omise # * Thailand self.supported_countries = %w(TH JP) # Credit cards supported by Omise # * VISA # * MasterCard # * JCB self.supported_cardtypes = %i[visa master jcb] # Omise main page self.homepage_url = 'https://www.omise.co/' self.display_name = 'Omise' # Creates a new OmiseGateway. # # Omise requires public_key for token creation. # And it requires secret_key for other transactions. # These keys can be found in https://dashboard.omise.co/test/api-keys # # ==== Options # # * :public_key -- Omise's public key (REQUIRED). # * :secret_key -- Omise's secret key (REQUIRED). # * :api_version -- Omise's API Version (OPTIONAL), default version is '2014-07-27' # See version at page https://dashboard.omise.co/api-version/edit def initialize(options = {}) requires!(options, :public_key, :secret_key) @public_key = options[:public_key] @secret_key = options[:secret_key] @api_version = options[:api_version] super end # Perform a purchase (with auto capture) # # ==== Parameters # # * money -- The purchasing amount in Thai Baht Satang # * payment_method -- The CreditCard object # * options -- An optional parameters, such as token from Omise.js # # ==== Options # * token_id -- token id, use Omise.js library to retrieve a token id # if this is passed as an option, it will ignore tokenizing via Omisevaultgateway object # # === Example # To create a charge on a card # # purchase(money, Creditcard_object) # # To create a charge on a token # # purchase(money, nil, { :token_id => token_id, ... }) # # To create a charge on a customer # # purchase(money, nil, { :customer_id => customer_id }) def purchase(money, payment_method, options = {}) create_charge(money, payment_method, options) end # Authorize a charge. # # ==== Parameters # # * money -- The purchasing amount in Thai Baht Satang # * payment_method -- The CreditCard object # * options -- An optional parameters, such as token or capture def authorize(money, payment_method, options = {}) options[:capture] = 'false' create_charge(money, payment_method, options) end # Capture an authorized charge. # # ==== Parameters # # * money -- An amount in Thai Baht Satang # * charge_id -- The CreditCard object # * options -- An optional parameters, such as token or capture def capture(money, charge_id, options = {}) post = {} add_amount(post, money, options) commit(:post, "charges/#{CGI.escape(charge_id)}/capture", post, options) end # Refund a charge. # # ==== Parameters # # * money -- An amount of money to charge in Satang. # * charge_id -- The CreditCard object # * options -- An optional parameters, such as token or capture def refund(money, charge_id, options = {}) options[:amount] = money if money commit(:post, "charges/#{CGI.escape(charge_id)}/refunds", options) end # Store a card details as customer # # ==== Parameters # # * payment_method -- The CreditCard. # * options -- Optional Customer information: # 'email' (A customer email) # 'description' (A customer description) def store(payment_method, options = {}) post, card_params = {}, {} add_customer_data(post, options) add_token(card_params, payment_method, options) commit(:post, 'customers', post.merge(card_params), options) end # Delete a customer and all associated credit cards. # # ==== Parameters # # * customer_id -- The Customer identifier (REQUIRED). def unstore(customer_id, options = {}) commit(:delete, "customers/#{CGI.escape(customer_id)}") end # Enable scrubbing sensitive information def supports_scrubbing? true end # Scrub sensitive information out of HTTP transcripts # # ==== Parameters # # * transcript -- The HTTP transcripts def scrub(transcript) transcript. gsub(/(Authorization: Basic )\w+/i, '\1[FILTERED]'). gsub(/(\\"number\\":)\\"\d+\\"/, '\1[FILTERED]'). gsub(/(\\"security_code\\":)\\"\d+\\"/, '\1[FILTERED]') end private def create_charge(money, payment_method, options) post = {} add_token(post, payment_method, options) add_amount(post, money, options) add_customer(post, options) post[:capture] = options[:capture] if options[:capture] commit(:post, 'charges', post, options) end def headers(options = {}) key = options[:key] || @secret_key { 'Content-Type' => 'application/json;utf-8', 'Omise-Version' => @api_version || '2014-07-27', 'User-Agent' => "ActiveMerchantBindings/#{ActiveMerchant::VERSION} Ruby/#{RUBY_VERSION}", 'Authorization' => 'Basic ' + Base64.encode64(key.to_s + ':').strip, 'Accept-Encoding' => 'utf-8' } end def url_for(endpoint) (endpoint == 'tokens' ? VAULT_URL : API_URL) + endpoint end def post_data(parameters) parameters.present? ? parameters.to_json : nil end def https_request(method, endpoint, parameters = nil, options = {}) raw_response = response = nil begin raw_response = ssl_request(method, url_for(endpoint), post_data(parameters), headers(options)) response = parse(raw_response) rescue ResponseError => e raw_response = e.response.body response = parse(raw_response) rescue JSON::ParserError response = json_error(raw_response) end response end def parse(body) JSON.parse(body) end def json_error(raw_response) msg = 'Invalid response received from Omise API. Please contact support@omise.co if you continue to receive this message.' msg += "The raw response returned by the API was #{raw_response.inspect})" { message: msg } end def commit(method, endpoint, params = nil, options = {}) response = https_request(method, endpoint, params, options) Response.new( successful?(response), message_from(response), response, { authorization: authorization_from(response), test: test?, error_code: successful?(response) ? nil : standard_error_code_mapping(response) } ) end def standard_error_code_mapping(response) STANDARD_ERROR_CODE_MAPPING[error_code_from(response)] || message_to_standard_error_code_from(response) end def error_code_from(response) error?(response) ? response['code'] : response['failure_code'] end def message_to_standard_error_code_from(response) message = response['message'] if response['code'] == 'invalid_card' case message when /brand not supported/ STANDARD_ERROR_CODE[:invalid_number] when /number is invalid/ STANDARD_ERROR_CODE[:incorrect_number] when /expiration date cannot be in the past/ STANDARD_ERROR_CODE[:expired_card] when /expiration \w+ is invalid/ STANDARD_ERROR_CODE[:invalid_expiry_date] else STANDARD_ERROR_CODE[:processing_error] end end def message_from(response) if successful?(response) 'Success' else response['message'] || response['failure_message'] end end def authorization_from(response) response['id'] if successful?(response) end def successful?(response) !error?(response) && response['failure_code'].nil? end def error?(response) response.key?('object') && (response['object'] == 'error') end def get_token(post, credit_card) add_creditcard(post, credit_card) if credit_card commit(:post, 'tokens', post, { key: @public_key }) end def add_token(post, credit_card, options = {}) if options[:token_id].present? post[:card] = options[:token_id] else response = get_token(post, credit_card) response.authorization ? (post[:card] = response.authorization) : response end end def add_creditcard(post, payment_method) card = { number: payment_method.number, name: payment_method.name, security_code: payment_method.verification_value, expiration_month: payment_method.month, expiration_year: payment_method.year } post[:card] = card end def add_customer(post, options = {}) post[:customer] = options[:customer_id] if options[:customer_id] end def add_customer_data(post, options = {}) post[:description] = options[:description] if options[:description] post[:email] = options[:email] if options[:email] end def add_amount(post, money, options) post[:amount] = amount(money) post[:currency] = (options[:currency] || currency(money)) post[:description] = options[:description] if options.key?(:description) end end end end