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

Add a PORO for creating tax adjustments #685

Merged
merged 9 commits into from Feb 3, 2016
Merged

Add a PORO for creating tax adjustments #685

merged 9 commits into from Feb 3, 2016

Conversation

mamhoff
Copy link
Contributor

@mamhoff mamhoff commented Jan 12, 2016

Adds a Spree::Tax::ItemAdjuster, which adjusts an item, and a Spree::Tax::OrderAdjuster, which iterates over line items and shipments in an order and adjusts those.

This PR is a prerequisite for any further taxation work.

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 12, 2016

Force-pushed with no changes because of spurious failure in admin shipment spec.

@jhawthorn
Copy link
Contributor

This does seem like a good target for extraction to a PORO, but the handling of the two cases (receiving either an item or an order) is a little awkward. The two cases share very little code.

Maybe we could adjust the signature of the adjuster? Brainstorming a little:

class Adjuster
  def initialize(order)
    @order = order
    @tax_zone = order.tax_zone
  end

  def adjust_order
    # adjust all items in the order
  end

  def adjust_item(item)
    # adjust just the item
  end
end

@tvdeyen
Copy link
Member

tvdeyen commented Jan 14, 2016

One adjuster for two different things? It seams the only thing they share is the order tax zone.

Why not an OrderAdjuster and a LineItemAdjuster, both knowing the order tax zone but having separated concerns?

Sandi Metz is calling! Excuse me ;)

@tvdeyen
Copy link
Member

tvdeyen commented Jan 14, 2016

There is too much knowledge of the two possible callers inside one class. Case statements on class types, then methods named after the caller, etc.

Two separate adjusters are much more flexible, more future safe and less error prone. If we use POROs than please let us use clean object oriented design.

Sorry, reading too much Sandi Metz lately.

Just my 0.02

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 15, 2016

Thank you for chipping in!

@jhawthorn Actually, they do share code, as the order adjuster doesn't actually adjust the order, but all the items (line items and shipments) contained within it.

The current stuff on TaxRate shaves some AR requests by only loading the potentially_applicable rates once per order - I would like that efficiency to stay, which is why I didn't separate the two into their own classes. @tvdeyen I hear you though. Possibly I can have the best of both worlds by making the LineItemAdjuster a Singleton and cache things like the tax rates in an instance variable.

At the moment, Spree::TaxRate.adjust is a method that is executed for side effects (create adjustments, update pre_tax_amount), which isn't super transparent. Ideally, I'd like something to this effect:

class Spree::LineItem
  has_many :adjustments, ... , autosave: true
  before_save :adjust

  private 

  # builds tax adjustments and sets pre tax amount, but doesn't actually save
  def adjust
    self = Spree::Tax::ItemAdjuster.new(self).adjust!
  end
end

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 16, 2016

Changed these to single responsibility objects - I do think we can deal with the efficiency after we've defined the interfaces.

order_tax_zone = self.tax_zone
Spree::TaxRate.adjust(order_tax_zone, line_items)
Spree::TaxRate.adjust(order_tax_zone, shipments) if shipments.any?
Spree::Tax::Adjuster.new(self).adjust!
Copy link
Member

Choose a reason for hiding this comment

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

Don't you want to call OrderAdjuster here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Absolutely. Thanks for the heads-up! 💯

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 19, 2016

Wheew! Green! Removing TaxRate.adjust, which is a monster, has not been that hard after all. I'm excited: I like the API, and because of some data sharing between the ItemAdjuster and the OrderAdjuster the expensive TaxRate.match call is only done once.

This is still lacking feedback. Once that's in, I'd start putting in unit tests and moving the Spree::TaxRate into some kind of taxation_integration_spec.rb file.

Please please, this isn't ready for merge, but definitely ready for feedback (although you do have a couple of days as I'll be out until the weekend from tomorrow).

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 23, 2016

Rebased on master, updated commit message on second commit. Feedback still welcome.

Btw: With #530 in, I'll take a go at removing TaxRate.match, too.

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 26, 2016

These last ones failed because of the Rails update (#736). Rebasing on master now, and adding a couple of performance fixes as well.

@mamhoff mamhoff changed the title WIP / RfC: Add a PORO for creating tax adjustments RfC: Add a PORO for creating tax adjustments Jan 26, 2016
@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 26, 2016

The build failed because of phantomjs crashing :(

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 26, 2016

Force-pushed to get a green build. Otherwise: This is ready for review.

I can think of more stuff to do, but the scope of this PR is already quite large, so I'll stop for now. :)

@peterberkenbosch
Copy link
Contributor

👍 Nice @mamhoff

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 26, 2016

Damn. The shipping rates also have VAT, and they're not price-adjusted yet.

@tvdeyen
Copy link
Member

tvdeyen commented Jan 27, 2016

I hopefully can spent some review time later this evening (CET)

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 27, 2016

This is really long, and hard to review. I'll split this PR in two: One that just adds the POROs and uses #530 to look up zones (that's all commits up to a68d9c8), and one that actually implements MOSS with that.

The one that actually implements MOSS will also have to deal with shipping rates, somehow.

@mamhoff mamhoff mentioned this pull request Jan 27, 2016
@tvdeyen
Copy link
Member

tvdeyen commented Jan 27, 2016

Alright


def initialize(item, options = {})
@item = item
@order = @item.order
Copy link
Member

Choose a reason for hiding this comment

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

@item and @order should be attr_readers, so the TaxHelper module and potential sub classes don't rely on reading instance variables.

@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 28, 2016

Did the naming changes, and also added some more tests to the adjusters.

For the time being, these two POROs (one for orders, another for items) call
the already defined methods on `Spree::TaxRate`.

The idea is, though, that the responsibilities between `Spree::Tax::{Item,Order}Adjuster` and
`Spree::TaxRate` are divided as follows: `Spree::TaxRate` stores and selects rates,
while `Spree::Tax::{Item,Order}Adjuster` create tax adjustments.
@mamhoff
Copy link
Contributor Author

mamhoff commented Jan 28, 2016

First commit was failing because of a naming change, rebased again.

The code in `Spree::TaxRate.adjust!` is hard to understand, and the
adjusting should happen in dedicated adjuster classes. This commit
ports the functionality of `TaxRate.match` over to two classes:

* An `OrderAdjuster` which iterates over all items (line items and shipments)
  in an order and runs the

* `ItemAdjuster`, which actually creates adjustments for every item.

The code should read a lot easier now.

In order to reduce database load, the tax rate array for an order can be passed
into the `ItemAdjuster.new`. Considering the performance benefits, I think
that's justifiable amount of shared knowledge between the two Adjuster classes.
TaxRate.for_zone will return all tax rates that are valid for a given zone.
It does not care about the default VAT zone, as that should be handled by the
Adjuster classes.
When using .with_shared_members, sometimes you pass nil - and that should just
yield an empty ActiveRecord relation.

Same goes for Spree::TaxRate.for_zone(nil).
This is really an intermediary step. I'm using for_zone already in
Spree::TaxRate.match, which should result in a constant eight DB calls per
.match call. Previously, the number of DB calls would depend on how many
tax rates there are, and how these are configured (as Spree::Zone.contains?
would be run for every tax rate in the system).

A side effect of this refactoring is that the reimbursement specs had to be
amended a little: The zones had to be created `:with_country`, because otherwise
there'd be no shared members and the tax rates wouldn't apply. This actually makes
the tests slightly more realistic (what is a zone without members, anyway?).

Also, I had to change an expectation: The included tax total of the reimbursement
was previously expected to be negative (which is something that IMO should never
happen, as negative included taxes at the moment become positive additional taxes).
The order adjuster and the item adjuster share code, because I need to run
some methods on the order level and cache them. This extracts these methods
into a module.

Inheritance would've also worked, but in this case I find a mixin
more fitting as the behaviour of the two classes is still quite different.
TaxRate.match does some pretty weird things, and I want to change them.
This commit moves that code to a temporary `applicable_rates` helper.
@mamhoff mamhoff changed the title RfC: Add a PORO for creating tax adjustments Add a PORO for creating tax adjustments Feb 2, 2016
@item = item
@order = @item.order
# set the instance variable so `TaxRate.match` is only called when necessary
@rates_for_order_zone = options[:rates_for_order_zone]
Copy link
Member

Choose a reason for hiding this comment

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

I'm not clear where this is being used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are used in private methods of the included module TaxHelpers, in order to run the database calls that only have to run once per order only once per order: https://github.com/magiclabs/solidus/blob/add-tax-adjuster/core/app/models/spree/tax/tax_helpers.rb#L28-L38.

@jhawthorn
Copy link
Contributor

Looks good to me. 👍

One thought, should we keep TaxRate.adjust as a deprecated method that would use the PORO?

@mamhoff
Copy link
Contributor Author

mamhoff commented Feb 2, 2016

Well, the method signatures are a bit different. I could use just the item adjuster, but then that method will have abysmal performance. I'd rather have it entirely gone. However, if there's another person saying: Deprecate it and be nice about it - I'll do it, along with the requested YARD docs tomorrow.

Add Spree::TaxRate.adjust as a deprecated method, and test for the deprecation
warnings. The deleted specs are covered in the specs for the ItemAdjuster.
tvdeyen pushed a commit that referenced this pull request Feb 3, 2016
Add a PORO for creating tax adjustments
@tvdeyen tvdeyen merged commit dac756c into solidusio:master Feb 3, 2016
@tvdeyen
Copy link
Member

tvdeyen commented Feb 3, 2016

🍦

@tonobo
Copy link

tonobo commented Feb 4, 2016

Nice ;)

@mamhoff mamhoff mentioned this pull request Mar 16, 2016
23 tasks
@mamhoff mamhoff deleted the add-tax-adjuster branch May 24, 2016 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants