Skip to content

Instantly share code, notes, and snippets.

@mbie
Created November 3, 2019 22:18
Show Gist options
  • Select an option

  • Save mbie/868dc816d4987253e606ceb124114111 to your computer and use it in GitHub Desktop.

Select an option

Save mbie/868dc816d4987253e606ceb124114111 to your computer and use it in GitHub Desktop.
RRUG#24 - Refactoring complex condition classes
# Retry payment conditions:
# - feature flag is enabled
# - only credit card and bank payments
# - there is no other payment processing for that customer
module Payments
class Retry
def call(payment:)
return unless retry_payment?(payment)
payment.retry! # or @retry_service.call(payment: payment), etc
log_history_log(payment)
update_metrics(payment)
end
private
def retry_payment?(payment)
ENV['PAYMENT_RETRIES_ENABLED'] == 'true' &&
(payment_type == :credit_card || payment_type == :bank_account) &&
Payment.where(customer: customer, status: :processing).empty?
end
end
end
RSpec.describe Payments::Retry do
it 'retries credit card payment' do
ENV['PAYMENT_RETRIES_ENABLED'] = 'true'
customer = create(:customer)
original_payment = create(:payment, :credit_card, :successful, customer: customer, date: 10.days.ago)
expect { Payments::Retry.call(payment: original_payment) }
.to change { Payment.where(original_payment: payment).count }
.by(1)
end
end
it 'retries credit card payment'
it 'retries bank account payment'
it 'does not retry payment if feature is disabled'
it 'does not retry payment if payment is debit card'
end
# Retry payment conditions:
# - feature flag is enabled
# - only credit card and bank payments
# - there is no other payment processing for that customer
# - only three retry attempts for cards and 2 for bank accounts
# - there is no more than 30 business days after previous failure
# - only Monday or Friday
module Payments
class Retry
CREDIT_CARD_RETRY_LIMIT = 3
BANK_ACCOUNT_RETRY_LIMIT = 2
def call(payment:)
return unless retry_payment?(payment)
payment.retry! # or @retry_service.call(payment: payment), etc
log_history_log(payment)
update_metrics(payment)
end
private
def retry_payment?(payment)
feature_enabled? &&
correct_payment_type?(payment.payment_type) &&
non_processing_payments_for?(payment.customer) &&
retry_limit_not_reached?(payment, payment.payment_type) &&
no_past_failures?(payment) &&
applicable_date?
end
def feature_enabled?
ENV['PAYMENT_RETRIES_ENABLED'] == 'true' # FEATURE_TOGGLE.on?(:payment_retries_enabled)
end
def correct_payment_type?(payment_type)
payment_type == :credit_card || payment_type == :bank_account
end
def non_processing_payments_for?(customer)
Payment.where(customer: customer, status: :processing).empty?
end
def retry_limit_not_reached?(payment, payment_type)
retry_limit =
case payment_type
when :credit_card then CREDIT_CARD_RETRY_LIMIT
when :bank_account then BANK_ACCOUNT_RETRY_LIMIT
end
retries_count = Payment.where(original_payment: payment).count
retry_limit < retries_count
end
def applicable_date?
current_date = Date.current
current_date.monday? || current_date.friday?
end
def no_past_failures?(payment)
# select 1
# from "payments"
# where "original_payment_id" = 2 AND
# "status" = 'failure' AND
# "date" < '2019-10-04'
Payment
.where(original_payment: payment, status: :failure)
.where(Payment.arel_table[:date].lt(30.days.ago))
.empty?
end
end
end
RSpec.describe Payments::Retry do
it 'retries credit card payment on Monday' do
ENV['PAYMENT_RETRIES_ENABLED'] = 'true'
customer = create(:customer)
original_payment = create(:payment, :credit_card, :successful, customer: customer, date: 10.days.ago)
first_retry_attempt = create(:payment, :credit_card, :failed, customer: customer, date: 1.day.ago, original_payment: original_payment)
date = Date.new(2019, 11, 4) # Random Monday
Timecop.travel(date) do
expect { Payments::Retry.call(payment: original_payment) }
.to change { Payment.where(original_payment: payment).count }
.by(1)
end
end
it 'retries credit card payment on Friday'
it 'retries bank account payment on Monday'
it 'retries bank account payment on Friday'
end
# Retry payment conditions:
# - feature flag is enabled
# - only credit card and bank payments
# - there is no other payment processing for that customer
# - only three retry attempts for cards and 2 for bank accounts
# - there is no more than 30 business days after previous failure
# - only Monday or Friday
module Payments
class Retry
def initialize(is_retry_possible: IsRetryPossible)
@is_retry_possible = is_retry_possible
end
def call(payment:)
return unless retry_payment?(payment)
payment.retry! # or @retry_service.call(payment: payment), etc
log_history_log(payment)
update_metrics(payment)
end
private
def retry_payment?(payment)
@is_retry_possible.call(payment_payment)
end
end
class IsRetryPossible
CREDIT_CARD_RETRY_LIMIT = 3
BANK_ACCOUNT_RETRY_LIMIT = 2
def call(payment:)
feature_enabled? &&
correct_payment_type?(payment.payment_type) &&
non_processing_payments_for?(payment.customer) &&
retry_limit_not_reached?(payment, payment.payment_type) &&
no_past_failures?(payment) &&
applicable_date?
end
def feature_enabled?
ENV['PAYMENT_RETRIES_ENABLED'] == 'true' # FEATURE_TOGGLE.on?(:payment_retries_enabled)
end
def correct_payment_type?(payment_type)
payment_type == :credit_card || payment_type == :bank_account
end
def non_processing_payments_for?(customer)
Payment.where(customer: customer, status: :processing).empty?
end
def retry_limit_not_reached?(payment, payment_type)
retry_limit =
case payment_type
when :credit_card then CREDIT_CARD_RETRY_LIMIT
when :bank_account then BANK_ACCOUNT_RETRY_LIMIT
end
retries_count = Payment.where(original_payment: payment).count
retry_limit < retries_count
end
def applicable_date?
current_date = Date.current
current_date.monday? || current_date.friday?
end
def no_past_failures?(payment)
# select 1
# from "payments"
# where "original_payment_id" = 2 AND
# "status" = 'failure' AND
# "date" < '2019-10-04'
Payment
.where(original_payment: payment, status: :failure)
.where(Payment.arel_table[:date].lt(30.days.ago))
.empty?
end
end
end
RSpec.describe Payments::RetryPayment do
it 'retries payment if it is possible' do
is_retry_possible = spy(:is_retry_possible)
original_payment = create(:payment, :credit_card, :successful, customer: customer, date: 10.days.ago)
allow(is_retry_possible).to receive(:call).with(payment: original_payment).and_return(true)
expect { Payments::Retry.call(payment: original_payment) }
.to change { Payment.where(original_payment: payment).count }
.by(1)
end
it 'creates history log'
it 'updates metrics'
it 'does not retry payment if it is not possible'
end
RSpec.describe Payments::IsRetryPossible do
it 'returns true for credit card payment on Monday' do
ENV['PAYMENT_RETRIES_ENABLED'] = 'true'
customer = create(:customer)
original_payment = create(:payment, :credit_card, :successful, customer: customer, date: 10.days.ago)
first_retry_attempt = create(:payment, :credit_card, :failed, customer: customer, date: 1.day.ago, original_payment: original_payment)
date = Date.new(2019, 11, 4) # Random Monday
Timecop.travel(date) do
expect(Payments::Retry.call(payment: original_payment)).to be(true)
end
end
it 'returns true for credit card payment on Friday'
it 'returns true for bank account payment on Monday'
it 'returns true for bank account payment on Friday'
end
# Retry payment conditions:
# - feature flag is enabled
# - only credit card and bank payments
# - there is no other payment processing for that customer
# - only three retry attempts for cards and 2 for bank accounts
# - there is no more than 30 business days after previous failure
# - only Monday or Friday
module Payments
module IsRetryPossible
class Check
def call(payment:)
feature_enabled? &&
correct_payment_type?(payment.payment_type) &&
non_processing_payments_for?(payment.customer) &&
retry_limit_not_reached?(payment, payment.payment_type) &&
no_past_failures?(payment) &&
applicable_date?
end
def feature_enabled?
Conditions::FeatureDisabled.call
end
def correct_payment_type?(payment_type)
Conditions::CorrectPaymentType.call(payment_type: payment_type)
end
def non_processing_payments_for?(customer)
Conditions::NonProcessingPaymentsFor.call(customer: customer)
end
def retry_limit_not_reached?(payment, payment_type)
Conditions::RetryLimitNotReached.call(payment: payment, payment_type: payment_type)
end
def applicable_date?
Conditions::ApplicableDate.call
end
def no_past_failures?(payment)
Conditions::NoPastFailures.call(payment: payment)
end
end
end
end
module Payments
module IsRetryPossible
module Conditions
class FeatureEnabled
def call
ENV['PAYMENT_RETRIES_ENABLED'] == 'true' # FEATURE_TOGGLE.on?(:payment_retries_enabled)
end
end
class CorrectPaymentType
def call(payment_type:)
payment_type == :credit_card || payment_type == :bank_account
end
end
class NonProcessingPayments
def call(customer:)
Payment.where(customer: customer, status: :processing).empty?
end
end
class RetryLimitNotReached
CREDIT_CARD_RETRY_LIMIT = 3
BANK_ACCOUNT_RETRY_LIMIT = 2
def call(payment:, payment_type:)
retry_limit =
case payment_type
when :credit_card then CREDIT_CARD_RETRY_LIMIT
when :bank_account then BANK_ACCOUNT_RETRY_LIMIT
end
retries_count = Payment.where(original_payment: payment).count
retry_limit < retries_count
end
end
class ApplicableDate
def call
current_date = Date.current
current_date.monday? || current_date.friday?
end
end
class NoPastFailures
def call(payment:)
# select 1
# from "payments"
# where "original_payment_id" = 2 AND
# "status" = 'failure' AND
# "date" < '2019-10-04'
Payment
.where(original_payment: payment, status: :failure)
.where(Payment.arel_table[:date].lt(30.days.ago))
.empty?
end
end
end
end
end
RSpec.describe Payments::IsRetryPossible::Conditions::CorrectPaymentType do
it 'returns true for credit cards' do
result = Payments::IsRetryPossible::Conditions::CorrectPaymentType.call(payment_type: :credit_card)
expect(result).to eq(true)
end
it 'returns false for prepaid cards' do
result = Payments::IsRetryPossible::Conditions::CorrectPaymentType.call(payment_type: :prepaid_card)
expect(result).to eq(false)
end
end
RSpec.describe Payments::IsRetryPossible::Conditions::NonProcessingPayments do
it 'returns true if there are not any processing payments' do
customer = create(:customer)
FactoryBot.create(:payment, :successful, customer: customer)
result = Payments::IsRetryPossible::Conditions::NonProcessingPayments.call(customer: customer)
expect(result).to eq(true)
end
end
RSpec.describe Payments::IsRetryPossible::Check do
# One happy path
it 'returns true if all conditions are met'
# One ore more failure path
it 'returns false if flag is disabled'
it 'returns false if date is Sunday'
end
# Retry payment conditions:
# - feature flag is enabled
# - only credit card and bank payments
# - there is no other payment processing for that customer
# - only three retry attempts for cards and 2 for bank accounts
# - there is no more than 30 business days after previous failure
# - only Monday or Friday
module Payments
module IsRetryPossible
class Check
def initialize(
feature_enabled: Conditions::FeatureEnabled,
correct_payment_type: Conditions::CorrectPaymentType,
non_processing_payments_for: Conditions::NonProcessingPaymentsFor,
retry_limit_not_reached: Conditions::RetryLimitNotReached,
no_past_failures: Conditions::NoPastFailures,
applicable_date: Conditions::ApplicableDate,
)
@feature_enabled = feature_enabled
@correct_payment_type = correct_payment_type
@non_processing_payments_for = non_processing_payments_for
@retry_limit_not_reached = retry_limit_not_reached
@no_past_failures = no_past_failures
@applicable_date = applicable_date
end
def call(payment:)
feature_enabled? &&
correct_payment_type?(payment.payment_type) &&
non_processing_payments_for?(payment.customer) &&
retry_limit_not_reached?(payment, payment.payment_type) &&
no_past_failures?(payment) &&
applicable_date?
end
def feature_enabled?
@feature_enabled.call
end
def correct_payment_type?(payment_type)
@correct_payment_type.call(payment_type: payment_type)
end
def non_processing_payments_for?(customer)
@non_processing_payments_for.call(customer: customer)
end
def retry_limit_not_reached?(payment, payment_type)
@retry_limit_not_reached.call(payment: payment, payment_type: payment_type)
end
def applicable_date?
@applicable_date.call
end
def no_past_failures?(payment)
@no_past_failures.call(payment: payment)
end
end
end
end
RSpec.describe Payments::IsRetryPossible::Check do
# One happy path
it 'returns true if all conditions are met'
# One ore more failure path
it 'returns false if flag is disabled'
it 'returns false if date is Sunday'
end
# Retry payment conditions:
# - feature flag is enabled
# - only credit card and bank payments
# - there is no other payment processing for that customer
# - only three retry attempts for cards and 2 for bank accounts
# - there is no more than 30 business days after previous failure
# - only Monday or Friday
module Payments
module IsRetryPossible
class Check
def initialize(
conditions: [
Conditions::FeatureEnabled,
Conditions::CorrectPaymentType,
Conditions::NonProcessingPaymentsFor,
Conditions::RetryLimitNotReached,
Conditions::NoPastFailures,
Conditions::ApplicableDate
]
)
@conditions = conditions
end
def call(payment:)
@conditions.all? do |condition|
condition.call(
payment: payment,
payment_type: payment.payment_type,
customer: payment.customer
)
end
end
end
end
end
module Payments
module IsRetryPossible
module Conditions
class FeatureEnabled
def call(**args)
ENV['PAYMENT_RETRIES_ENABLED'] == 'true' # FEATURE_TOGGLE.on?(:payment_retries_enabled)
end
end
class CorrectPaymentType
def call(payment_type:, **args)
payment_type == :credit_card || payment_type == :bank_account
end
end
class NonProcessingPayments
def call(customer:, **args)
Payment.where(customer: customer, status: :processing).empty?
end
end
end
end
end
RSpec.describe Payments::IsRetryPossible::Check do
# One happy path (integration level)
it 'returns true if all conditions are met'
# Testing managing conditions
it 'returns true if one condition returns true' do
true_condition = -> { true }
false_condition = -> { false }
payment = double(:payment)
conditions = [true_condition]
result = Payments::IsRetryPossible::Check.new(conditions: conditions).call(payment: payment)
expect(result).to eq(true)
end
it 'returns false if one condition returns false' do
true_condition = -> { true }
false_condition = -> { false }
payment = double(:payment)
conditions = [true_condition, true_condition, false_condition]
result = Payments::IsRetryPossible::Check.new(conditions: conditions).call(payment: payment)
expect(result).to eq(false)
end
# etc
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment