mirror of
https://github.com/openfoodfoundation/openfoodnetwork
synced 2026-03-06 02:51:34 +00:00
The original payment may not be valid because its credit card may be expired. Stripe gives this as a valid scenario returning a success and we should do too. When creating the credit payment we end up validating all sources in a chain as follows. ``` Payment being persisted -> source payment -> original credit card. ``` The source payment was valid when created (It would not be persisted otherwise) but its source card may now be expired, and that's legit. There was also an issue with the `#invalidate_old_payments` callback. It was causing the original payment to be validated again and thus the credit payment failed to be persisted due to the original credit card being expired. Switching this callback to use `#update_column` skips validations and so we don't validate the source payment. We only care about the state there, so it should be fine.
275 lines
8.8 KiB
Ruby
275 lines
8.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Spree
|
|
class Payment < ActiveRecord::Base
|
|
module Processing
|
|
def process!
|
|
return unless payment_method&.source_required?
|
|
|
|
raise Core::GatewayError, Spree.t(:payment_processing_failed) unless source
|
|
|
|
return if processing?
|
|
|
|
unless payment_method.supports?(source)
|
|
invalidate!
|
|
raise Core::GatewayError, Spree.t(:payment_method_not_supported)
|
|
end
|
|
|
|
if payment_method.auto_capture?
|
|
purchase!
|
|
else
|
|
authorize!
|
|
end
|
|
end
|
|
|
|
def authorize!
|
|
started_processing!
|
|
gateway_action(source, :authorize, :pend)
|
|
end
|
|
|
|
def purchase!
|
|
started_processing!
|
|
gateway_action(source, :purchase, :complete)
|
|
end
|
|
|
|
def capture!
|
|
return true if completed?
|
|
|
|
started_processing!
|
|
protect_from_connection_error do
|
|
check_environment
|
|
|
|
response = if payment_method.payment_profiles_supported?
|
|
# Gateways supporting payment profiles will need access to credit
|
|
# card object because this stores the payment profile information
|
|
# so supply the authorization itself as well as the credit card,
|
|
# rather than just the authorization code
|
|
payment_method.capture(self, source, gateway_options)
|
|
else
|
|
# Standard ActiveMerchant capture usage
|
|
payment_method.capture(money.money.cents,
|
|
response_code,
|
|
gateway_options)
|
|
end
|
|
|
|
handle_response(response, :complete, :failure)
|
|
end
|
|
end
|
|
|
|
def void_transaction!
|
|
return true if void?
|
|
|
|
protect_from_connection_error do
|
|
check_environment
|
|
|
|
response = if payment_method.payment_profiles_supported?
|
|
# Gateways supporting payment profiles will need access to credit
|
|
# card object because this stores the payment profile information
|
|
# so supply the authorization itself as well as the credit card,
|
|
# rather than just the authorization code
|
|
payment_method.void(response_code, source, gateway_options)
|
|
else
|
|
# Standard ActiveMerchant void usage
|
|
payment_method.void(response_code, gateway_options)
|
|
end
|
|
|
|
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 = if payment_method.payment_profiles_supported?
|
|
payment_method.credit(
|
|
(credit_amount * 100).round,
|
|
source,
|
|
response_code,
|
|
gateway_options
|
|
)
|
|
else
|
|
payment_method.credit(
|
|
(credit_amount * 100).round,
|
|
response_code,
|
|
gateway_options
|
|
)
|
|
end
|
|
|
|
record_response(response)
|
|
|
|
if response.success?
|
|
self.class.create!(
|
|
order: order,
|
|
source: self,
|
|
payment_method: 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 = if payment_method.payment_profiles_supported?
|
|
payment_method.refund(
|
|
(refund_amount * 100).round,
|
|
source,
|
|
response_code,
|
|
gateway_options
|
|
)
|
|
else
|
|
payment_method.refund(
|
|
(refund_amount * 100).round,
|
|
response_code,
|
|
gateway_options
|
|
)
|
|
end
|
|
|
|
record_response(response)
|
|
|
|
if response.success?
|
|
self.class.create(order: order,
|
|
source: self,
|
|
payment_method: payment_method,
|
|
amount: refund_amount.abs * -1,
|
|
response_code: response.authorization,
|
|
state: 'completed')
|
|
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.tax_total * 100,
|
|
subtotal: order.item_total * 100,
|
|
discount: order.promo_total * 100,
|
|
currency: currency })
|
|
|
|
options.merge!({ billing_address: order.bill_address.try(:active_merchant_hash),
|
|
shipping_address: order.ship_address.try(:active_merchant_hash) })
|
|
|
|
options
|
|
end
|
|
|
|
private
|
|
|
|
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)
|
|
protect_from_connection_error do
|
|
check_environment
|
|
|
|
response = payment_method.public_send(
|
|
action,
|
|
(amount * 100).round,
|
|
source,
|
|
gateway_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']
|
|
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.params['message'] || error.params['response_reason_text'] || error.message
|
|
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
|
|
|
|
# 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
|