-
-
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 all 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,201 @@ | ||
# frozen_string_literal: true | ||
|
||
module Spree | ||
class Payment < ActiveRecord::Base | ||
include Spree::Payment::Processing | ||
extend Spree::LocalizedNumber | ||
|
||
localize_number :amount | ||
|
||
IDENTIFIER_CHARS = (('A'..'Z').to_a + ('0'..'9').to_a - %w(0 1 I O)).freeze | ||
|
||
delegate :line_items, to: :order | ||
delegate :currency, to: :order | ||
|
||
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").completed }, | ||
class_name: "Spree::Payment", foreign_key: :source_id | ||
has_many :log_entries, as: :source, dependent: :destroy | ||
|
||
has_one :adjustment, as: :source, dependent: :destroy | ||
|
||
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)) } | ||
|
||
# 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 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.positive? | ||
end | ||
|
||
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 | ||
|
||
# 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 | ||
return [] unless payment_source&.respond_to?(:actions) | ||
|
||
actions = payment_source.actions.select do |action| | ||
!payment_source.respond_to?("can_#{action}?") || | ||
payment_source.__send__("can_#{action}?", self) | ||
end | ||
|
||
if payment_method.is_a? Gateway::Pin | ||
actions << 'refund' if actions.include? 'credit' | ||
actions.reject! { |a| ['credit', 'void'].include? a } | ||
end | ||
|
||
actions | ||
end | ||
|
||
def payment_source | ||
res = source.is_a?(Payment) ? source.source : source | ||
res || payment_method | ||
end | ||
|
||
def ensure_correct_adjustment | ||
revoke_adjustment_eligibility if ['failed', 'invalid'].include?(state) | ||
return if adjustment.try(:finalized?) | ||
|
||
if adjustment | ||
adjustment.originator = payment_method | ||
adjustment.label = adjustment_label | ||
adjustment.save | ||
else | ||
payment_method.create_adjustment(adjustment_label, order, self, true) | ||
association(:adjustment).reload | ||
end | ||
end | ||
|
||
def adjustment_label | ||
I18n.t('payment_method_fee') | ||
end | ||
|
||
private | ||
|
||
# Don't charge fees for invalid or failed payments. | ||
# This is called twice for failed payments, because the persistence of the 'failed' | ||
# state is acheived through some trickery using an after_rollback callback on the | ||
# payment model. See Spree::Payment#persist_invalid | ||
def revoke_adjustment_eligibility | ||
return unless adjustment.try(:reload) | ||
return if adjustment.finalized? | ||
|
||
adjustment.update_attribute(:eligible, false) | ||
adjustment.finalize! | ||
end | ||
|
||
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}") | ||
errors.add(Spree.t(source.class.to_s.demodulize.underscore), "#{field_name} #{error}") | ||
end | ||
end | ||
errors.blank? | ||
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) | ||
return unless source.try(:save_requested_by_customer?) | ||
return unless source.number || source.gateway_payment_profile_id | ||
return unless source.gateway_customer_profile_id.nil? | ||
|
||
payment_method.create_profile(self) | ||
rescue ActiveMerchant::ConnectionError => e | ||
gateway_error e | ||
end | ||
|
||
def invalidate_old_payments | ||
order.payments.with_state('checkout').where("id != ?", id).each(&:invalidate!) | ||
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 | ||
self.identifier = generate_identifier while self.class.exists?(identifier: identifier) | ||
end | ||
|
||
def generate_identifier | ||
Array.new(8){ IDENTIFIER_CHARS.sample }.join | ||
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.
Looking at this now I see this could be done in pin.rb instead of here. Not in scope for this PR.