From 5f83ffa326a56a3981680e477929d372be404415 Mon Sep 17 00:00:00 2001 From: Graeme Porteous Date: Tue, 8 Oct 2024 09:36:00 +0100 Subject: [PATCH 01/22] Extract Pro WithTax into Taxable concern Replacing wrapped Stripe objects with new Pro Plan which includes Taxable. Pro Plan will be expanded in later commits while refactoring the checkout process. --- .../alaveteli_pro/plans_controller.rb | 4 +- app/helpers/alaveteli_pro/account_helper.rb | 2 +- app/models/alaveteli_pro/plan.rb | 8 ++++ app/models/alaveteli_pro/with_tax.rb | 29 -------------- app/models/concerns/taxable.rb | 40 +++++++++++++++++++ .../{with_tax_spec.rb => plan_spec.rb} | 2 +- .../plans/index.html.erb_spec.rb | 4 +- 7 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 app/models/alaveteli_pro/plan.rb delete mode 100644 app/models/alaveteli_pro/with_tax.rb create mode 100644 app/models/concerns/taxable.rb rename spec/models/alaveteli_pro/{with_tax_spec.rb => plan_spec.rb} (94%) diff --git a/app/controllers/alaveteli_pro/plans_controller.rb b/app/controllers/alaveteli_pro/plans_controller.rb index fde8cdfd79..61d9033568 100644 --- a/app/controllers/alaveteli_pro/plans_controller.rb +++ b/app/controllers/alaveteli_pro/plans_controller.rb @@ -7,7 +7,7 @@ class AlaveteliPro::PlansController < AlaveteliPro::BaseController def index default_plan_name = add_stripe_namespace('pro') stripe_plan = Stripe::Plan.retrieve(default_plan_name) - @plan = AlaveteliPro::WithTax.new(stripe_plan) + @plan = AlaveteliPro::Plan.new(stripe_plan) @pro_site_name = pro_site_name end @@ -15,7 +15,7 @@ def show stripe_plan = Stripe::Plan.retrieve( id: plan_name, expand: ['product'] ) - @plan = AlaveteliPro::WithTax.new(stripe_plan) + @plan = AlaveteliPro::Plan.new(stripe_plan) rescue Stripe::InvalidRequestError raise ActiveRecord::RecordNotFound end diff --git a/app/helpers/alaveteli_pro/account_helper.rb b/app/helpers/alaveteli_pro/account_helper.rb index 6828b3f8e2..ee2901f0f6 100644 --- a/app/helpers/alaveteli_pro/account_helper.rb +++ b/app/helpers/alaveteli_pro/account_helper.rb @@ -19,6 +19,6 @@ def card_expiry_message(month, year) end def subscription_amount(subscription) - AlaveteliPro::WithTax.new(subscription).amount_with_tax + AlaveteliPro::Plan.new(subscription.plan).amount_with_tax end end diff --git a/app/models/alaveteli_pro/plan.rb b/app/models/alaveteli_pro/plan.rb new file mode 100644 index 0000000000..6aa7b9c2d8 --- /dev/null +++ b/app/models/alaveteli_pro/plan.rb @@ -0,0 +1,8 @@ +## +# A wrapper for a Stripe::Plan +# +class AlaveteliPro::Plan < SimpleDelegator + include Taxable + + tax :amount +end diff --git a/app/models/alaveteli_pro/with_tax.rb b/app/models/alaveteli_pro/with_tax.rb deleted file mode 100644 index 8d1f4b1edb..0000000000 --- a/app/models/alaveteli_pro/with_tax.rb +++ /dev/null @@ -1,29 +0,0 @@ -# -# Calculate amount + tax for a Stripe::Plan or Stripe::Subscription. -# -# Set STRIPE_TAX_RATE in config/general.yml to change the tax rate. -# -# Example -# -# plan = Stripe::Plan.retrieve('pro') -# @plan = WithTax.new(plan) -# @plan.amount -# # => 833 -# @plan.amount_with_tax -# # => 1000 -class AlaveteliPro::WithTax < SimpleDelegator - def amount_with_tax - # Need to use BigDecimal() here because SimpleDelegator is forwarding - # `#BigDecimal` to `#amount` in Ruby 2.0. - net = BigDecimal(amount * 0.01, 0).round(2) - vat = (net * tax_rate).round(2) - gross = net + vat - (gross * 100).floor - end - - private - - def tax_rate - @tax_rate ||= BigDecimal(AlaveteliConfiguration.stripe_tax_rate) - end -end diff --git a/app/models/concerns/taxable.rb b/app/models/concerns/taxable.rb new file mode 100644 index 0000000000..20b011b121 --- /dev/null +++ b/app/models/concerns/taxable.rb @@ -0,0 +1,40 @@ +## +# Calculate amount + tax for an attribute +# +# Set STRIPE_TAX_RATE in config/general.yml to change the tax rate. +# +# Example +# klass = Struct.new(:amount).include(Taxable) +# klass.tax(:amount) +# instance = klass.new(833) +# instance.amount +# # => 833 +# instance.amount_with_tax +# # => 1000 +# +module Taxable + extend ActiveSupport::Concern + + included do + class << self + def tax(*attributes) + attributes.each do |attribute| + define_method("#{attribute}_with_tax") do + # Need to use BigDecimal() here because SimpleDelegator is forwarding + # `#BigDecimal` to `#amount` in Ruby 2.0. + net = BigDecimal(send(attribute) * 0.01, 0).round(2) + vat = (net * tax_rate).round(2) + gross = net + vat + (gross * 100).floor + end + end + end + end + end + + private + + def tax_rate + @tax_rate ||= BigDecimal(AlaveteliConfiguration.stripe_tax_rate) + end +end diff --git a/spec/models/alaveteli_pro/with_tax_spec.rb b/spec/models/alaveteli_pro/plan_spec.rb similarity index 94% rename from spec/models/alaveteli_pro/with_tax_spec.rb rename to spec/models/alaveteli_pro/plan_spec.rb index 5c0aba59ff..bffd0abdde 100644 --- a/spec/models/alaveteli_pro/with_tax_spec.rb +++ b/spec/models/alaveteli_pro/plan_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -RSpec.describe AlaveteliPro::WithTax do +RSpec.describe AlaveteliPro::Plan do let(:plan) { double(:plan, amount: 833) } subject { described_class.new(plan) } diff --git a/spec/views/alaveteli_pro/plans/index.html.erb_spec.rb b/spec/views/alaveteli_pro/plans/index.html.erb_spec.rb index 68b5ec0da6..7261098758 100644 --- a/spec/views/alaveteli_pro/plans/index.html.erb_spec.rb +++ b/spec/views/alaveteli_pro/plans/index.html.erb_spec.rb @@ -6,7 +6,7 @@ let(:stripe_helper) { StripeMock.create_test_helper } let(:product) { stripe_helper.create_product } - let(:plan_with_tax) { AlaveteliPro::WithTax.new(stripe_plan) } + let(:plan) { AlaveteliPro::Plan.new(stripe_plan) } let(:cents_price) { 880 } let(:stripe_plan) do @@ -18,7 +18,7 @@ before do allow(AlaveteliConfiguration).to receive(:iso_currency_code). and_return('GBP') - assign :plan, plan_with_tax + assign :plan, plan assign :pro_site_name, 'AlaveteliPro' end From 69dfb917e4b8995add1048cad58e94f3a76be273 Mon Sep 17 00:00:00 2001 From: Graeme Porteous Date: Mon, 11 Nov 2024 12:06:14 +0000 Subject: [PATCH 02/22] Update Taxable concern We're always returning integers there is no need to convert to floats and back again. This can introduce rounding errors. --- app/models/concerns/taxable.rb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/app/models/concerns/taxable.rb b/app/models/concerns/taxable.rb index 20b011b121..cc63db5ba1 100644 --- a/app/models/concerns/taxable.rb +++ b/app/models/concerns/taxable.rb @@ -20,12 +20,9 @@ class << self def tax(*attributes) attributes.each do |attribute| define_method("#{attribute}_with_tax") do - # Need to use BigDecimal() here because SimpleDelegator is forwarding - # `#BigDecimal` to `#amount` in Ruby 2.0. - net = BigDecimal(send(attribute) * 0.01, 0).round(2) - vat = (net * tax_rate).round(2) - gross = net + vat - (gross * 100).floor + net = send(attribute) + vat = (net * tax_rate).round(0) + net + vat end end end @@ -35,6 +32,6 @@ def tax(*attributes) private def tax_rate - @tax_rate ||= BigDecimal(AlaveteliConfiguration.stripe_tax_rate) + @tax_rate ||= AlaveteliConfiguration.stripe_tax_rate.to_f end end From 0ee00bcbb94156d29e2498597b54a9d5dfc9bd98 Mon Sep 17 00:00:00 2001 From: Graeme Porteous Date: Tue, 8 Oct 2024 10:06:34 +0100 Subject: [PATCH 03/22] Update Pro Subscription method_missing Check in object respond to the method before forwarding. This is needed to fix exceptions such as: private method `BigDecimal' called for # Date: Tue, 8 Oct 2024 10:11:06 +0100 Subject: [PATCH 04/22] Add Pro Subscription#plan method Load and cache subscription.plan, automatically wrapping in our Pro Plan class. --- app/helpers/alaveteli_pro/account_helper.rb | 4 ---- app/models/alaveteli_pro/subscription.rb | 5 +++++ app/models/alaveteli_pro/subscription_with_discount.rb | 1 - app/views/alaveteli_pro/subscriptions/_subscription.html.erb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/helpers/alaveteli_pro/account_helper.rb b/app/helpers/alaveteli_pro/account_helper.rb index ee2901f0f6..ab9d526026 100644 --- a/app/helpers/alaveteli_pro/account_helper.rb +++ b/app/helpers/alaveteli_pro/account_helper.rb @@ -17,8 +17,4 @@ def card_expiry_message(month, year) content_tag(:p, _('Expires soon'), class: 'card__expiring') end end - - def subscription_amount(subscription) - AlaveteliPro::Plan.new(subscription.plan).amount_with_tax - end end diff --git a/app/models/alaveteli_pro/subscription.rb b/app/models/alaveteli_pro/subscription.rb index a6ea687ef5..1a7816d4c6 100644 --- a/app/models/alaveteli_pro/subscription.rb +++ b/app/models/alaveteli_pro/subscription.rb @@ -45,6 +45,11 @@ def delete Stripe::Subscription.cancel(id) end + # plan + def plan + @plan ||= AlaveteliPro::Plan.new(__getobj__.plan) + end + private def method_missing(method, *args, &block) diff --git a/app/models/alaveteli_pro/subscription_with_discount.rb b/app/models/alaveteli_pro/subscription_with_discount.rb index b257c2d33d..37e1d069f8 100644 --- a/app/models/alaveteli_pro/subscription_with_discount.rb +++ b/app/models/alaveteli_pro/subscription_with_discount.rb @@ -19,7 +19,6 @@ class AlaveteliPro::SubscriptionWithDiscount < SimpleDelegator def initialize(subscription) super - @plan = subscription.plan @original_amount = subscription.plan.amount @discount = subscription.discount @coupon = fetch_coupon diff --git a/app/views/alaveteli_pro/subscriptions/_subscription.html.erb b/app/views/alaveteli_pro/subscriptions/_subscription.html.erb index 24115cbcc2..ce6ee7e9ce 100644 --- a/app/views/alaveteli_pro/subscriptions/_subscription.html.erb +++ b/app/views/alaveteli_pro/subscriptions/_subscription.html.erb @@ -9,7 +9,7 @@