Recently I picked up a ticket from support team of one of our clients. Few months ago VAT rates have changed in Norway - 5% became 10% and 12% became 15%. It has some implications to platform users — event organizers, since they can choose which VAT rate applies to products which they offer to the ticket buyers. You’ll learn why I haven’t just updated db column.
Current state of the app
This app is a great example of a legacy software. It’s successful, earns a lot of money, but have some areas of code which haven’t been cleaned yet. There’s a concept of an organization in codebase, which represents the given country market. The organization has an attribute called
available_vat_rates which is simply a serialized attribute, keeping
VatRate value objects. I won’t focus on this object here, since its implementation is not a point of this post. It works in a really simple manner:
vat_rate = VatRate.new(15) # => #<VatRate:0x007fdb8ed50db0 @value=15.0, @code="15"> vat_rate.code # => "15" vat_rate.to_d => #<BigDecimal:7fdb8ed716c8,'0.15E2',9(27)>
VatRate objects are
Comparable so you can easily sort them; pretty neat solution.
Event organizer, who creates eg. a ticket, can choose a valid VAT rate applying to his product. Then, after purchase is made, ticket buyer receives the e-mail with a receipt. This has also side-effects in the financial reporting, obviously.
So what’s the problem?
I could simply write a migration and add new VAT rates, remove old ones and update events’ products which use old rates. However, no domain knowledge would be handed down about when change was made and what kind of change happened. You simply can’t get that information from
updated_at column in your database. We have nice domain facts “coverage” around event concept in the application, so we’re well informed here. We don’t have such knowledge in regard to the Organization.
Start with a plan
I simply started with making a plan of this upgrade.
- I’ve checked if the change made to
available_vat_rateswill be represented properly in the financial reports.
- I’ve checked how many products were having old VAT rates set.
- I’ve introduced new domain events called
Organization::VatRateRemovedwhich are published to the
- I’ve run a migration, which was adding new VAT rates (10% & 15%) and publishing sufficient domain facts — let’s call it step 1.
- I’ve performed an upgrade of the VAT rates on the products which required it - step 2.
- I’ve run a migration, which has removed old VAT rates (5% & 12%) and published domain facts - step 3.
Step 1 - adding new VAT rates
require 'event_store' class AddNewVatRatesToNoOrgazation < ActiveRecord::Migration #… minimum viable implementations of used classes def up event_store = Rails.application.config.event_store organization = Organization.find_by(domain: 'me.no') originator_id = User.find_by(email: '[email protected]').id organization.available_vat_rates = [ VatRate.new('NoVat'), VatRate.new(5), # deprecated one VatRate.new(10), # new one VatRate.new(12), # deprecated one VatRate.new(15), # new one VatRate.new(25), ] if organization.save event_store.publish(Organization::VatRateAdded.new( organization: organization.id, vat_rate_code: 10, originator_id: originator_id ) event_store.publish(Organization::VatRateAdded.new( organization: organization.id, vat_rate_code: 15, originator_id: originator_id ) end end end
Two things worth notice happen here. Event data contain
originator_id, I simply passed there my
user_id. Just to leave other team members information about person who performed the change in the event store — audit log purpose. The second thing is that I leave old VAT rates still available. Just in case if any event organizer performing changes on his products, to prevent errors and partially migrated state.
Step 2 - migrating affected product data
The amount of products which required change of the VAT rates was so small that I simply used web interface to update them. Normally I would just go with baking
UpdateTicketTypeCommand containing all the necessary data.
Step 3 - remove deprecated VAT rates
require 'event_store' class RemoveOldVatRatesFromNoOrgazation < ActiveRecord::Migration #… minimum viable implementations of used classes def up event_store = Rails.application.config.event_store organization = Organization.find_by(domain: 'me.no') originator_id = User.find_by(email: '[email protected]').id organization.available_vat_rates = [ VatRate.new('NoVat'), VatRate.new(10), VatRate.new(15), VatRate.new(25), ] if organization.save event_store.publish(Organization::VatRateRemoved.new( organization: organization.id, vat_rate_code: 5, originator_id: originator_id ) event_store.publish(Organization::VatRateRemoved.new( organization: organization.id, vat_rate_code: 12, originator_id: originator_id ) end end end
All the products on the platform have proper VAT rates set, organization has proper list of available VAT rates. And last, but not least, we know what and when exactly happened, we have better domain understanding, we started publishing events for another bounded context of our app. If you’re still not convinced to publishing domain events, please read Andrzej post on that topic or even better, by watching his keynote From Rails legacy to DDD performed on wroc_love.rb.