I'm develping a rails app that will have separate instances run in different countries. Internationalization takes care of the language issues, but when there are some minor logical changes I'm not sure how to approach. One example would be the payment methods - in each country different options will be shown to the users with different integrations. Another examples would be salaries - taxes and social securities are completely different from country to country so we'll need to take this into account.
The differences are not that many so in general I don't want to create a different git branch for each country, but I might be wrong about that.
The solution I'm currently thinking of is to have a different environment for each country - "production_germany", "production_france" etc. and depending on that to load a different yml file with all the necessary variables that I'll be needing for this country. Then create some "Manager" class that will make decisions what special classes to load and what things to show on the views(wherever there are differences). For example I can have the salary calculation classes for germany in "salary_calculation/de" and for france in "salary_calculation/fr" and load only the ones I need.
What would be your solution?
I would handle as much as possible using Rails' I18n
. In case you haven't yet, also take a look at rails-i18n.
For your special cases that aren't handled by Rails, I would go about it like Rails does itself. For example, in your config/locales
, you might have some configuration for taxes in a country:
en:
taxes:
name: Tax
rate: 0.065
de:
taxes:
name: USt
rate: 0.19
Then you write a TaxesHelper
module that will use the information from the locale file to display the tax rate. Take a look at the Rails NumberHelper
package.
If you actually do calculations with something like tax rates, I would persist the factor in the model where you're doing the calculation. That way your calculations won't be messed up when the tax rate changes. Say you have the following model:
class Invoice < ActiveRecord::Base
# Injected by AR
# attr_accessor :net_price
# attr_accessor :tax_rate
def gross_price
net_price * tax_rate
end
before_create do
tax_rate = I18n.t(:rate, scope: [ :taxes ]).to_f
end
end
That way your gross price won't change for persisted invoices after the tax rate changes in a specific country.
Amiuhle gave a good tip on how to handle static parameterization - languages, tax rates etc,
when you need more business logic then things get more complicated, and there's no 1-size-fits-all solution, just be sure to know tools at your disposal and make sure you fit the right one, and here's one more that seems to be a bit underused in the ruby community:
For example: http://solnic.eu/2013/12/17/the-world-needs-another-post-about-dependency-injection-in-ruby.html
This pattern is typically good for managing payment provider gateways for example
class Transaction < ActiveRecord::Base
def gateway_class
I18n.t(:class_name, scope: [ :payment_provider ]).constantize
end
def gateway
@gateway ||= gateway_class.new(self)
end
end
the I18n parametrizes the payment provider class depending on the country:
en:
payment_provider:
class_name: PaymentProvider
de:
payment_provider:
class_name: AnotherPaymentProvider
and you make sure that the payment provider class answers to the same public API so for the whole app it's not necessary to know which payment provider is used, just that a payment provider class is interfaced.
Another good thing is that you can create a FakePaymentProvider
class for testing and development which will speed up your tests a lot - even if you would normally use a web mocking tool such as vcr
, and you can still use something like vcr
to test integrations, and just integrations, decoupled from the rest of the app architecture.
Actually, take a look here: https://github.com/activemerchant/active_merchant somebody already did a bunch of work on that plan :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With