app/models/spree/gateway/amazon.rb
##
# Amazon Payments - Login and Pay for Spree Commerce
#
# @category Amazon
# @package Amazon_Payments
# @copyright Copyright (c) 2014 Amazon.com
# @license http://opensource.org/licenses/Apache-2.0 Apache License, Version 2.0
#
##
module Spree
class Gateway::Amazon < Gateway
REGIONS = %w(us uk de jp).freeze
preference :currency, :string, default: -> { Spree::Config.currency }
preference :client_id, :string
preference :merchant_id, :string
preference :aws_access_key_id, :string
preference :aws_secret_access_key, :string
preference :region, :string, default: 'us'
validates :preferred_region, inclusion: { in: REGIONS }
def self.for_currency(currency)
where(active: true).detect { |gateway| gateway.preferred_currency == currency }
end
def api_url
sandbox = preferred_test_mode ? '_Sandbox' : ''
{
'us' => "https://mws.amazonservices.com/OffAmazonPayments#{sandbox}/2013-01-01",
'uk' => "https://mws-eu.amazonservices.com/OffAmazonPayments#{sandbox}/2013-01-01",
'de' => "https://mws-eu.amazonservices.com/OffAmazonPayments#{sandbox}/2013-01-01",
'jp' => "https://mws.amazonservices.jp/OffAmazonPayments#{sandbox}/2013-01-01",
}.fetch(preferred_region)
end
def widgets_url
sandbox = preferred_test_mode ? '/sandbox' : ''
{
'us' => "https://static-na.payments-amazon.com/OffAmazonPayments/us#{sandbox}/js/Widgets.js",
'uk' => "https://static-eu.payments-amazon.com/OffAmazonPayments/uk#{sandbox}/lpa/js/Widgets.js",
'de' => "https://static-eu.payments-amazon.com/OffAmazonPayments/de#{sandbox}/lpa/js/Widgets.js",
'jp' => "https://origin-na.ssl-images-amazon.com/images/G/09/EP/offAmazonPayments#{sandbox}/prod/lpa/js/Widgets.js",
}.fetch(preferred_region)
end
def supports?(source)
true
end
if Spree.solidus_gem_version >= Gem::Version.new('2.3.x')
def partial_name
'amazon'
end
else
def method_type
'amazon'
end
end
def provider_class
AmazonTransaction
end
def payment_source_class
AmazonTransaction
end
def source_required?
true
end
def authorize(amount, amazon_checkout, gateway_options={})
if amount < 0
return ActiveMerchant::Billing::Response.new(true, "Success", {})
end
order_number, payment_number = extract_order_and_payment_number(gateway_options)
order = Spree::Order.find_by!(number: order_number)
payment = Spree::Payment.find_by!(number: payment_number)
authorization_reference_id = operation_unique_id(payment)
load_amazon_mws(order.amazon_order_reference_id)
mws_res = begin
@mws.authorize(
authorization_reference_id,
amount / 100.0,
order.currency,
seller_authorization_note: sandbox_authorize_simulation_string(order),
)
rescue RuntimeError => e
raise Spree::Core::GatewayError.new(e.to_s)
end
amazon_response = SpreeAmazon::Response::Authorization.new(mws_res)
parsed_response = amazon_response.parse rescue nil
if amazon_response.success?
if amazon_response.state == 'Open'
success = true
order.amazon_transaction.update!(
authorization_id: amazon_response.response_id
)
message = 'Success'
else
success = false
message = "Authorization failure: #{amazon_response.reason_code}"
end
else
success = false
if !amazon_response.body.nil? && amazon_response.error_response_present?
message = "#{amazon_response.response_code} #{amazon_response.error_code}: #{amazon_response.error_message}"
else
message = "#{mws_res.code} #{mws_res.body}"
end
end
ActiveMerchant::Billing::Response.new(
success,
message,
{
'response' => mws_res,
'parsed_response' => parsed_response,
}
)
end
def capture(amount, amazon_checkout, gateway_options={})
if amount < 0
return credit(amount.abs, nil, nil, gateway_options)
end
order_number, payment_number = extract_order_and_payment_number(gateway_options)
order = Spree::Order.find_by!(number: order_number)
payment = Spree::Payment.find_by!(number: payment_number)
authorization_id = order.amazon_transaction.authorization_id
capture_reference_id = operation_unique_id(payment)
load_amazon_mws(order.amazon_order_reference_id)
mws_res = @mws.capture(authorization_id, capture_reference_id, amount / 100.00, order.currency)
response = SpreeAmazon::Response::Capture.new(mws_res)
t = order.amazon_transaction
t.capture_id = response.response_id
t.save!
return ActiveMerchant::Billing::Response.new(response.success_state?, "OK",
{
'response' => mws_res,
'parsed_response' => response.parse,
}
)
end
def purchase(amount, amazon_checkout, gateway_options={})
auth_result = authorize(amount, amazon_checkout, gateway_options)
if auth_result.success?
capture(amount, amazon_checkout, gateway_options)
else
auth_result
end
end
def credit(amount, _response_code, gateway_options = {})
payment = gateway_options[:originator].payment
amazon_transaction = payment.source
load_amazon_mws(amazon_transaction.order_reference)
response = @mws.refund(
amazon_transaction.capture_id,
operation_unique_id(payment),
amount / 100.00,
payment.currency
)
return ActiveMerchant::Billing::Response.new(true, "Success", Hash.from_xml(response.body))
end
def void(response_code, gateway_options)
order = Spree::Order.find_by(:number => gateway_options[:order_id].split("-")[0])
load_amazon_mws(order.amazon_order_reference_id)
capture_id = order.amazon_transaction.capture_id
if capture_id.nil?
response = @mws.cancel
else
response = @mws.refund(capture_id, gateway_options[:order_id], order.total, order.currency)
end
return ActiveMerchant::Billing::Response.new(true, "Success", Hash.from_xml(response.body))
end
def cancel(response_code)
payment = Spree::Payment.find_by!(response_code: response_code)
order = payment.order
load_amazon_mws(payment.source.order_reference)
capture_id = order.amazon_transaction.capture_id
if capture_id.nil?
response = @mws.cancel
else
response = @mws.refund(response_code, order.number, payment.credit_allowed, payment.currency)
end
return ActiveMerchant::Billing::Response.new(true, "#{order.number}-cancel", Hash.from_xml(response.body))
end
private
def load_amazon_mws(reference)
@mws ||= AmazonMws.new(reference, gateway: self)
end
def extract_order_and_payment_number(gateway_options)
gateway_options[:order_id].split("-", 2)
end
# Amazon requires unique ids. Calling with the same id multiple times means
# the result of the previous call will be returned again. This can be good
# for things like asynchronous retries, but would break things like multiple
# captures on a single authorization.
def operation_unique_id(payment)
"#{payment.number}-#{random_suffix}"
end
# A random string of lowercase alphanumeric characters (i.e. "base 36")
def random_suffix
length = 10
SecureRandom.random_number(36 ** length).to_s(36).rjust(length, '0')
end
# Allows simulating errors in sandbox mode if the *last* name of the
# shipping address is "SandboxSimulation" and the *first* name is one of:
#
# InvalidPaymentMethodHard-<minutes> (-<minutes> is optional. between 1-240.)
# InvalidPaymentMethodSoft-<minutes> (-<minutes> is optional. between 1-240.)
# AmazonRejected
# TransactionTimedOut
# ExpiredUnused-<minutes> (-<minutes> is optional. between 1-60.)
# AmazonClosed
#
# E.g. a full name like: "AmazonRejected SandboxSimulation"
#
# See https://payments.amazon.com/documentation/lpwa/201956480 for more
# details on Amazon Payments Sandbox Simulations.
def sandbox_authorize_simulation_string(order)
return nil if !preferred_test_mode
return nil if order.ship_address.nil?
return nil if order.ship_address.lastname != 'SandboxSimulation'
reason, minutes = order.ship_address.firstname.to_s.split('-', 2)
# minutes is optional and is only used for some of the reason codes
minutes ||= '1'
case reason
when 'InvalidPaymentMethodHard' then %({"SandboxSimulation": {"State":"Declined", "ReasonCode":"InvalidPaymentMethod", "PaymentMethodUpdateTimeInMins":#{minutes}}})
when 'InvalidPaymentMethodSoft' then %({"SandboxSimulation": {"State":"Declined", "ReasonCode":"InvalidPayment Method", "PaymentMethodUpdateTimeInMins":#{minutes}, "SoftDecline":"true"}})
when 'AmazonRejected' then '{"SandboxSimulation": {"State":"Declined", "ReasonCode":"AmazonRejected"}}'
when 'TransactionTimedOut' then '{"SandboxSimulation": {"State":"Declined", "ReasonCode":"TransactionTimedOut"}}'
when 'ExpiredUnused' then %({"SandboxSimulation": {"State":"Closed", "ReasonCode":"ExpiredUnused", "ExpirationTimeInMins":#{minutes}}})
when 'AmazonClosed' then '{"SandboxSimulation": {"State":"Closed", "ReasonCode":"AmazonClosed"}}'
else
Rails.logger.error('"SandboxSimulation" was given as the shipping first name but the last name was not a valid reason code: ' + order.ship_address.firstname.inspect)
nil
end
end
end
end