Skip to content
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
Closed
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
06aa561
Bring in Payment model from Spree
sauloperez Jun 26, 2020
abacd06
Fix credit card instance in specs
sauloperez Jun 26, 2020
e1ea5db
Fix all but the 7 last payment specs
sauloperez Jun 26, 2020
34de219
Bring in missing translation
sauloperez Jun 26, 2020
a01f601
Fix yet another spec
sauloperez Jun 26, 2020
0ad8dcc
Fix payment log entries specs
sauloperez Jun 26, 2020
9935df9
Move Pin payment method from decorator into model
sauloperez Jun 26, 2020
31d0d4b
Fix error "no parent is saved"
sauloperez Jun 26, 2020
eafaa97
Temporarily skip spec
sauloperez Jun 26, 2020
322c4d0
Move decorator's callbacks to model
sauloperez Jun 26, 2020
6d9a518
Move method from decorator to model
sauloperez Jun 26, 2020
48910ae
Move #refund! to the processing.rb
sauloperez Jun 26, 2020
8617262
Move localize_number from decorator to model
sauloperez Jun 26, 2020
3fb6193
Move adjustments logic from decorator into model
sauloperez Jun 26, 2020
cf6138d
Replace model method with its decorated version
sauloperez Jun 26, 2020
d49068c
Move method delegation from decorator to model
sauloperez Jun 26, 2020
d8b748a
Merge alias_method method and its original version
sauloperez Jun 26, 2020
8fbbb0b
Bring back our card factory modification
sauloperez Jul 10, 2020
562f397
Isolate Spree's specs into their own context
sauloperez Jul 10, 2020
2f46483
Merge decorator specs with Spree's ones
sauloperez Jul 10, 2020
6837946
Rename spec file
sauloperez Jul 10, 2020
55d52b8
Run rubocop autocorrect on payment model
sauloperez Jul 10, 2020
cf64d3a
Merge skipped callback from decorator into model
sauloperez Jul 10, 2020
3435d5a
Fix Rubocop non-metrics issues in payment model
sauloperez Jul 10, 2020
66dbd85
Run rubocop autocorrect on payment/processing.rb
sauloperez Jul 10, 2020
42658b5
Refactor `#process!` nested ifs to guard clauses
sauloperez Jul 13, 2020
a8af3a2
Fix all but Metrics Rubocop cops in processing.rb
sauloperez Jul 13, 2020
3a64cc4
Reuse #calculate_refund_amount method
sauloperez Jul 13, 2020
70afcee
Fix Spree's spec clashing with a customization
sauloperez Jul 15, 2020
dd5e679
Address code review comments
sauloperez Jul 16, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions app/models/spree/payment.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# 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

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

delegate :currency, to: :order
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was converted from method to delegate by rubocop, probably nicer to move it up next to the other delegate?


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

# see https://github.com/spree/spree/issues/981
#
# Import from future Spree v.2.3.0 d470b31798f37
Copy link
Contributor

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.

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
Copy link
Contributor

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.

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
Loading