-
-
Notifications
You must be signed in to change notification settings - Fork 725
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bring in payment model #5678
Closed
sauloperez
wants to merge
30
commits into
openfoodfoundation:master
from
coopdevs:bring-in-payment-model
Closed
Bring in payment model #5678
Changes from 11 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
06aa561
Bring in Payment model from Spree
sauloperez abacd06
Fix credit card instance in specs
sauloperez e1ea5db
Fix all but the 7 last payment specs
sauloperez 34de219
Bring in missing translation
sauloperez a01f601
Fix yet another spec
sauloperez 0ad8dcc
Fix payment log entries specs
sauloperez 9935df9
Move Pin payment method from decorator into model
sauloperez 31d0d4b
Fix error "no parent is saved"
sauloperez eafaa97
Temporarily skip spec
sauloperez 322c4d0
Move decorator's callbacks to model
sauloperez 6d9a518
Move method from decorator to model
sauloperez 48910ae
Move #refund! to the processing.rb
sauloperez 8617262
Move localize_number from decorator to model
sauloperez 3fb6193
Move adjustments logic from decorator into model
sauloperez cf6138d
Replace model method with its decorated version
sauloperez d49068c
Move method delegation from decorator to model
sauloperez d8b748a
Merge alias_method method and its original version
sauloperez 8fbbb0b
Bring back our card factory modification
sauloperez 562f397
Isolate Spree's specs into their own context
sauloperez 2f46483
Merge decorator specs with Spree's ones
sauloperez 6837946
Rename spec file
sauloperez 55d52b8
Run rubocop autocorrect on payment model
sauloperez cf64d3a
Merge skipped callback from decorator into model
sauloperez 3435d5a
Fix Rubocop non-metrics issues in payment model
sauloperez 66dbd85
Run rubocop autocorrect on payment/processing.rb
sauloperez 42658b5
Refactor `#process!` nested ifs to guard clauses
sauloperez a8af3a2
Fix all but Metrics Rubocop cops in processing.rb
sauloperez 3a64cc4
Reuse #calculate_refund_amount method
sauloperez 70afcee
Fix Spree's spec clashing with a customization
sauloperez dd5e679
Address code review comments
sauloperez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
module Spree | ||
class Payment < ActiveRecord::Base | ||
include Spree::Payment::Processing | ||
|
||
IDENTIFIER_CHARS = (('A'..'Z').to_a + ('0'..'9').to_a - %w(0 1 I O)).freeze | ||
|
||
belongs_to :order, class_name: 'Spree::Order' | ||
belongs_to :source, polymorphic: true | ||
belongs_to :payment_method, class_name: 'Spree::PaymentMethod' | ||
|
||
has_many :offsets, -> { where("source_type = 'Spree::Payment' AND amount < 0 AND state = 'completed'") }, | ||
class_name: "Spree::Payment", foreign_key: :source_id | ||
has_many :log_entries, as: :source | ||
|
||
before_validation :validate_source | ||
before_save :set_unique_identifier | ||
|
||
after_save :create_payment_profile, if: :profiles_supported? | ||
|
||
# update the order totals, etc. | ||
after_save :ensure_correct_adjustment, :update_order | ||
# invalidate previously entered payments | ||
after_create :invalidate_old_payments | ||
|
||
attr_accessor :source_attributes | ||
after_initialize :build_source | ||
|
||
scope :from_credit_card, -> { where(source_type: 'Spree::CreditCard') } | ||
scope :with_state, ->(s) { where(state: s.to_s) } | ||
scope :completed, -> { with_state('completed') } | ||
scope :pending, -> { with_state('pending') } | ||
scope :failed, -> { with_state('failed') } | ||
scope :valid, -> { where('state NOT IN (?)', %w(failed invalid)) } | ||
|
||
after_rollback :persist_invalid | ||
|
||
def persist_invalid | ||
return unless ['failed', 'invalid'].include?(state) | ||
state_will_change! | ||
save | ||
end | ||
|
||
# order state machine (see http://github.com/pluginaweek/state_machine/tree/master for details) | ||
state_machine initial: :checkout do | ||
# With card payments, happens before purchase or authorization happens | ||
event :started_processing do | ||
transition from: [:checkout, :pending, :completed, :processing], to: :processing | ||
end | ||
# When processing during checkout fails | ||
event :failure do | ||
transition from: [:pending, :processing], to: :failed | ||
end | ||
# With card payments this represents authorizing the payment | ||
event :pend do | ||
transition from: [:checkout, :processing], to: :pending | ||
end | ||
# With card payments this represents completing a purchase or capture transaction | ||
event :complete do | ||
transition from: [:processing, :pending, :checkout], to: :completed | ||
end | ||
event :void do | ||
transition from: [:pending, :completed, :checkout], to: :void | ||
end | ||
# when the card brand isnt supported | ||
event :invalidate do | ||
transition from: [:checkout], to: :invalid | ||
end | ||
end | ||
|
||
def currency | ||
order.currency | ||
end | ||
|
||
def money | ||
Spree::Money.new(amount, { currency: currency }) | ||
end | ||
alias display_amount money | ||
|
||
def offsets_total | ||
offsets.pluck(:amount).sum | ||
end | ||
|
||
def credit_allowed | ||
amount - offsets_total | ||
end | ||
|
||
def can_credit? | ||
credit_allowed > 0 | ||
end | ||
|
||
# see https://github.com/spree/spree/issues/981 | ||
# | ||
# Import from future Spree v.2.3.0 d470b31798f37 | ||
def build_source | ||
return if source_attributes.nil? | ||
return unless payment_method.andand.payment_source_class | ||
|
||
self.source = payment_method.payment_source_class.new(source_attributes) | ||
source.payment_method_id = payment_method.id | ||
source.user_id = order.user_id if order | ||
end | ||
|
||
def actions | ||
return [] unless payment_source and payment_source.respond_to? :actions | ||
payment_source.actions.select { |action| !payment_source.respond_to?("can_#{action}?") or payment_source.send("can_#{action}?", self) } | ||
end | ||
|
||
# Pin payments lacks void and credit methods, but it does have refund | ||
# Here we swap credit out for refund and remove void as a possible action | ||
def actions_with_pin_payment_adaptations | ||
actions = actions_without_pin_payment_adaptations | ||
if payment_method.is_a? Gateway::Pin | ||
actions << 'refund' if actions.include? 'credit' | ||
actions.reject! { |a| ['credit', 'void'].include? a } | ||
end | ||
actions | ||
end | ||
alias_method_chain :actions, :pin_payment_adaptations | ||
|
||
def payment_source | ||
res = source.is_a?(Payment) ? source.source : source | ||
res || payment_method | ||
end | ||
|
||
private | ||
|
||
def validate_source | ||
if source && !source.valid? | ||
source.errors.each do |field, error| | ||
field_name = I18n.t("activerecord.attributes.#{source.class.to_s.underscore}.#{field}") | ||
self.errors.add(Spree.t(source.class.to_s.demodulize.underscore), "#{field_name} #{error}") | ||
end | ||
end | ||
return !errors.present? | ||
end | ||
|
||
def profiles_supported? | ||
payment_method.respond_to?(:payment_profiles_supported?) && payment_method.payment_profiles_supported? | ||
end | ||
|
||
def create_payment_profile | ||
return unless source.is_a?(CreditCard) && source.number && !source.has_payment_profile? | ||
payment_method.create_profile(self) | ||
rescue ActiveMerchant::ConnectionError => e | ||
gateway_error e | ||
end | ||
|
||
def invalidate_old_payments | ||
order.payments.with_state('checkout').where("id != ?", self.id).each do |payment| | ||
payment.invalidate! | ||
end | ||
end | ||
|
||
def update_order | ||
order.payments.reload | ||
order.update! | ||
end | ||
|
||
# Necessary because some payment gateways will refuse payments with | ||
# duplicate IDs. We *were* using the Order number, but that's set once and | ||
# is unchanging. What we need is a unique identifier on a per-payment basis, | ||
# and this is it. Related to #1998. | ||
# See https://github.com/spree/spree/issues/1998#issuecomment-12869105 | ||
def set_unique_identifier | ||
begin | ||
self.identifier = generate_identifier | ||
end while self.class.exists?(identifier: self.identifier) | ||
end | ||
|
||
def generate_identifier | ||
Array.new(8){ IDENTIFIER_CHARS.sample }.join | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
module Spree | ||
class Payment < ActiveRecord::Base | ||
module Processing | ||
def process! | ||
if payment_method && payment_method.source_required? | ||
if source | ||
if !processing? | ||
if payment_method.supports?(source) | ||
if payment_method.auto_capture? | ||
purchase! | ||
else | ||
authorize! | ||
end | ||
else | ||
invalidate! | ||
raise Core::GatewayError.new(Spree.t(:payment_method_not_supported)) | ||
end | ||
end | ||
else | ||
raise Core::GatewayError.new(Spree.t(:payment_processing_failed)) | ||
end | ||
end | ||
sauloperez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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! | ||
sauloperez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
protect_from_connection_error do | ||
check_environment | ||
|
||
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 | ||
response = payment_method.capture(self, source, gateway_options) | ||
else | ||
# Standard ActiveMerchant capture usage | ||
response = 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 | ||
|
||
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 | ||
response = payment_method.void(self.response_code, source, gateway_options) | ||
else | ||
# Standard ActiveMerchant void usage | ||
response = payment_method.void(self.response_code, gateway_options) | ||
end | ||
record_response(response) | ||
|
||
if response.success? | ||
self.response_code = response.authorization | ||
self.void | ||
else | ||
gateway_error(response) | ||
end | ||
end | ||
end | ||
|
||
def credit!(credit_amount=nil) | ||
protect_from_connection_error do | ||
check_environment | ||
|
||
credit_amount ||= credit_allowed >= order.outstanding_balance.abs ? order.outstanding_balance.abs : credit_allowed.abs | ||
credit_amount = credit_amount.to_f | ||
|
||
if payment_method.payment_profiles_supported? | ||
response = payment_method.credit((credit_amount * 100).round, source, response_code, gateway_options) | ||
else | ||
response = 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' | ||
) | ||
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 gateway_action(source, action, success_state) | ||
protect_from_connection_error do | ||
check_environment | ||
|
||
response = payment_method.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 | ||
self.send("#{success_state}!") | ||
else | ||
self.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 | ||
begin | ||
yield | ||
rescue ActiveMerchant::ConnectionError => e | ||
gateway_error(e) | ||
end | ||
end | ||
|
||
def gateway_error(error) | ||
if error.is_a? ActiveMerchant::Billing::Response | ||
text = error.params['message'] || error.params['response_reason_text'] || error.message | ||
elsif error.is_a? ActiveMerchant::ConnectionError | ||
text = Spree.t(:unable_to_connect_to_gateway) | ||
else | ||
text = error.to_s | ||
end | ||
logger.error(Spree.t(:gateway_error)) | ||
logger.error(" #{error.to_yaml}") | ||
raise Core::GatewayError.new(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.new(message) | ||
end | ||
|
||
# The unique identifier to be passed in to the payment gateway | ||
def gateway_order_id | ||
"#{order.number}-#{self.identifier}" | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd just remove these 2 comments. It's just history, I dont see them as useful for the future dev.