mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-02-22 00:57:26 +00:00
The StripeSCA method is forwarding all missing methods to the provider
gateway. The ActiveMerchant gateway used to be decorated by our code and
that's how this code may have worked one day. But we removed the
decorator years ago:
- 549610bc35
A bit later we found that refunds are broken with Stripe:
- https://github.com/openfoodfoundation/openfoodnetwork/issues/12843
This commit could potentiall fix that, I guess? I haven't tested that.
It's a rare use case and not the aim of this work.
278 lines
7.7 KiB
Ruby
278 lines
7.7 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Spree
|
|
class Payment < ApplicationRecord
|
|
module Processing
|
|
def process!
|
|
return unless validate!
|
|
|
|
purchase!
|
|
end
|
|
|
|
def process_offline!
|
|
return unless validate!
|
|
return if requires_authorization?
|
|
|
|
if preauthorized?
|
|
capture!
|
|
else
|
|
charge_offline!
|
|
end
|
|
end
|
|
|
|
def authorize!(return_url = nil)
|
|
started_processing!
|
|
gateway_action(source, :authorize, :pend, return_url:)
|
|
end
|
|
|
|
def purchase!
|
|
started_processing!
|
|
gateway_action(source, :purchase, :complete)
|
|
end
|
|
|
|
def charge_offline!
|
|
started_processing!
|
|
gateway_action(source, :charge_offline, :complete)
|
|
end
|
|
|
|
def capture!
|
|
return true if completed?
|
|
|
|
started_processing!
|
|
protect_from_connection_error do
|
|
check_environment
|
|
response = payment_method.capture(money.money.cents, response_code, gateway_options)
|
|
|
|
handle_response(response, :complete, :failure)
|
|
end
|
|
end
|
|
|
|
def capture_and_complete_order!
|
|
Orders::WorkflowService.new(order).complete!
|
|
capture!
|
|
end
|
|
|
|
def void_transaction!
|
|
return true if void?
|
|
|
|
protect_from_connection_error do
|
|
check_environment
|
|
|
|
response = payment_method.void(response_code, gateway_options)
|
|
|
|
record_response(response)
|
|
|
|
if response.success?
|
|
self.response_code = response.authorization
|
|
void
|
|
else
|
|
gateway_error(response)
|
|
end
|
|
end
|
|
end
|
|
|
|
def credit!(credit_amount = nil)
|
|
protect_from_connection_error do
|
|
check_environment
|
|
|
|
credit_amount = calculate_refund_amount(credit_amount)
|
|
|
|
response = payment_method.credit(
|
|
(credit_amount * 100).round,
|
|
response_code,
|
|
gateway_options
|
|
)
|
|
|
|
record_response(response)
|
|
|
|
if response.success?
|
|
self.class.create!(
|
|
order:,
|
|
source: self,
|
|
payment_method:,
|
|
amount: credit_amount.abs * -1,
|
|
response_code: response.authorization,
|
|
state: 'completed',
|
|
skip_source_validation: true
|
|
)
|
|
else
|
|
gateway_error(response)
|
|
end
|
|
end
|
|
end
|
|
|
|
def refund!(refund_amount = nil)
|
|
protect_from_connection_error do
|
|
check_environment
|
|
|
|
refund_amount = calculate_refund_amount(refund_amount)
|
|
|
|
response = payment_method.refund(
|
|
(refund_amount * 100).round,
|
|
response_code,
|
|
gateway_options
|
|
)
|
|
|
|
record_response(response)
|
|
|
|
if response.success?
|
|
self.class.create!(
|
|
order:,
|
|
source: self,
|
|
payment_method:,
|
|
amount: refund_amount.abs * -1,
|
|
response_code: response.authorization,
|
|
state: 'completed',
|
|
skip_source_validation: true
|
|
)
|
|
else
|
|
gateway_error(response)
|
|
end
|
|
end
|
|
end
|
|
|
|
def partial_credit(amount)
|
|
return if amount > credit_allowed
|
|
|
|
started_processing!
|
|
credit!(amount)
|
|
end
|
|
|
|
def gateway_options
|
|
options = { email: order.email,
|
|
customer: order.email,
|
|
ip: order.last_ip_address,
|
|
# Need to pass in a unique identifier here to make some
|
|
# payment gateways happy.
|
|
#
|
|
# For more information, please see Spree::Payment#set_unique_identifier
|
|
order_id: gateway_order_id }
|
|
|
|
options.merge!(shipping: order.ship_total * 100,
|
|
tax: order.additional_tax_total * 100,
|
|
subtotal: order.item_total * 100,
|
|
discount: 0,
|
|
currency:)
|
|
|
|
options.merge!({ billing_address: order.bill_address.try(:active_merchant_hash),
|
|
shipping_address: order.ship_address.try(:active_merchant_hash) })
|
|
options.merge!(payment: self)
|
|
|
|
options
|
|
end
|
|
|
|
private
|
|
|
|
def preauthorized?
|
|
response_code.presence&.match("pi_")
|
|
end
|
|
|
|
def validate!
|
|
return false unless payment_method&.source_required?
|
|
|
|
raise Core::GatewayError, Spree.t(:payment_processing_failed) unless source
|
|
|
|
return false if processing?
|
|
|
|
unless payment_method.supports?(source)
|
|
invalidate!
|
|
raise Core::GatewayError, Spree.t(:payment_method_not_supported)
|
|
end
|
|
true
|
|
end
|
|
|
|
def calculate_refund_amount(refund_amount = nil)
|
|
refund_amount ||= if credit_allowed >= order.outstanding_balance.abs
|
|
order.outstanding_balance.abs
|
|
else
|
|
credit_allowed.abs
|
|
end
|
|
refund_amount.to_f
|
|
end
|
|
|
|
def gateway_action(source, action, success_state, options = {})
|
|
protect_from_connection_error do
|
|
check_environment
|
|
|
|
response = payment_method.public_send(
|
|
action,
|
|
(amount * 100).round,
|
|
source,
|
|
gateway_options.merge(options)
|
|
)
|
|
handle_response(response, success_state, :failure)
|
|
end
|
|
end
|
|
|
|
def handle_response(response, success_state, failure_state)
|
|
record_response(response)
|
|
|
|
if response.success?
|
|
unless response.authorization.nil?
|
|
self.response_code = response.authorization
|
|
self.avs_response = response.avs_result['code']
|
|
|
|
if response.cvv_result
|
|
self.cvv_response_code = response.cvv_result['code']
|
|
self.cvv_response_message = response.cvv_result['message']
|
|
self.redirect_auth_url = response.cvv_result['redirect_auth_url']
|
|
if redirect_auth_url.present?
|
|
return require_authorization!
|
|
end
|
|
end
|
|
end
|
|
__send__("#{success_state}!")
|
|
else
|
|
__send__(failure_state)
|
|
gateway_error(response)
|
|
end
|
|
end
|
|
|
|
def record_response(response)
|
|
log_entries.create(details: response.to_yaml)
|
|
end
|
|
|
|
def protect_from_connection_error
|
|
yield
|
|
rescue ActiveMerchant::ConnectionError => e
|
|
gateway_error(e)
|
|
end
|
|
|
|
def gateway_error(error)
|
|
text = if error.is_a? ActiveMerchant::Billing::Response
|
|
error_text(error)
|
|
elsif error.is_a? ActiveMerchant::ConnectionError
|
|
Spree.t(:unable_to_connect_to_gateway)
|
|
else
|
|
error.to_s
|
|
end
|
|
logger.error(Spree.t(:gateway_error))
|
|
logger.error(" #{error.to_yaml}")
|
|
raise Core::GatewayError, text
|
|
end
|
|
|
|
def error_text(error)
|
|
if (code = error.params.dig('error', 'code')) && I18n.exists?("stripe.error_code.#{code}")
|
|
I18n.t("stripe.error_code.#{code}")
|
|
else
|
|
error.params['message'] || error.params['response_reason_text'] || error.message
|
|
end
|
|
end
|
|
|
|
# Saftey check to make sure we're not accidentally performing operations on a live gateway.
|
|
# Ex. When testing in staging environment with a copy of production data.
|
|
def check_environment
|
|
return if payment_method.environment == Rails.env
|
|
|
|
message = Spree.t(:gateway_config_unavailable) + " - #{Rails.env}"
|
|
raise Core::GatewayError, message
|
|
end
|
|
|
|
# The unique identifier to be passed in to the payment gateway
|
|
def gateway_order_id
|
|
"#{order.number}-#{identifier}"
|
|
end
|
|
end
|
|
end
|
|
end
|