Created
November 3, 2019 22:18
-
-
Save mbie/868dc816d4987253e606ceb124114111 to your computer and use it in GitHub Desktop.
RRUG#24 - Refactoring complex condition classes
This file contains hidden or 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
| # 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 |
This file contains hidden or 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
| # 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 |
This file contains hidden or 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
| # 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 |
This file contains hidden or 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
| # 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 |
This file contains hidden or 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
| # 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 |
This file contains hidden or 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
| # 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