Recently, one of our customers (ListMinut) asked us to help them open their market to a new country : France.
Their platform is a marketplace allowing people to find service providers for small jobs like baby-sitting, plumbing, or gardening. The location is therefore extremely important and they wanted the french users to browse a France scoped site while keeping the current experience the same for the belgian users.
They also wanted to keep a single administration panel and also not fork the codebase in order to easily fix bugs or make global changes in the future.
We had a meeting with them in order to plan the evolution of the app and prioritize steps based on the criticity of the task and the external constraints.
After some discussions we agreed with them on the following steps :
- The app must be able to tell the country of a request.
- The bank account format must be validated differently depending on the country
- The national identification number must be validated differently depending on the country
- The locations must be searched only for the current country
- The addresses suggestions (using google place autocomplete ) must be scoped for the current country
- The payment gateway must propose different payment methods depending on the country
- The urls in the emails sent by the application must use the correct hostname
- The job categories proposed must be different from one country to another
- The language switcher must only propose languages of the country or must be hidden if the country only has one language
- The admin panel must propose a combinable filter on the country (which means all the current filters can optionally be combined with a country)
- The newsletter must be different from one country to another
- The highlighted reviews exposed on the home page must have been posted by people of the same country
- The default prices of a job category must depend upon the country
- The cache must take the country into account when relevant
Some of these changes were structural ones while other were much simpler. Let's review the most interesting changes.
The app must be able to tell the country of a request.
Obviously nothing can be done until we can tell if a request comes from France or Belgium or any other country the system might handle in the future.
Since the french users will access the website using www.listminut.fr and the current belgian users will access it using www.listminut.be, the straightforward solution is to check the hostname and choose a country based on the tld.
The problem with this approach is that it will only work in production. Whenever we are in dev, test, staging or any other environment than production we won't have a tld at our disposal. We could use a trick on a development machine but it would not work for staging or ci environment.
Thus we decided to set a chain of places to lookup for a country hint and this chain would end with the TLD. Before that we would check for a custom cookie (easily settable with a browser plugin) or even a query_string param.
Technically speaking, we thought this solution was very close to the one of setting the locale of the request and added a
before_action in our
HasLocale controller concern. This also enabled us to choose the default locale depending on the country
From this moment we were able to call
current_country from any controller and be sure to get a
Some validations depend on the country.
This looked like a tricky one but ended up easier than we though.
Our first idea was to pass the request country down to the validation process like some kind of context. But something felt wrong and we realized going down that path would mean passing the country along in a lot of calls. Then we thought about stroring the country in some kind of thread/request specific variable (like
I18n.locale) but we didn't like that idea either because it is just a disguised constant in terms of dependency management; and actually most of us do not like how any piece of code can access
Then we walked a step back and realise the country should be an associated record of some of our main object : a
User is deeply associated with a country (at least in this project), a
Worker also and so is a
Job and an
Address. Validating correctly a social security number, a bank account IBAN or a VAT number was jut a matter of delegating the validation to the indirectly associated coutry.
The main problem was resolved but we were left with a more tricky one : countries were instances of a
Country class but different instances needed different implementation of a same method. the
belgium.verify_national_id_number(user, national_number) should be different than
france.verify_national_id_number(user, national_number) event if those two objects were of the same
This is a typical data vs code problem and we solve it using the well-known strategy pattern
Once users were associated with countries, we easily found solutions for the email urls problem and the separate newsletter.
The job categories must be different from one country to another
This one was probably the most interesting for us. The initial codebase was several years old and was authored by one senior developer and several trainees over the year. This meant that the categories were displayed on several parts of the webapps and almost everytime in a different fashion. Most of the times the categories were even requested from the database directly in the template.
We solved the problem in 3 steps:
- Create a PORO
CategoryTree representing a browseable (Enumerable) tree of category
- Use that
CategoryTree everywhere it could be used.
- Add factory methods to build country specific or level specific (categories are either primary or secondary) trees
It turned out to be very effective and even allowed us to fix some bugs and dramatically improve the performance on most pages because our tree was buildable in a pair of request instead of N+1.
Since part of the app is build on angular 2, we also build a
The rest of the problems were either straightforward (change some controller actions by using the newly provided
current_country) or mostly non technical (contact the payment gateway provider to propose payment mechanisms based on the country).
In the end we managed to do the expected work in the tight schedule (around one man month) and our client has been able to launch its service in Paris as expected.
It was also very interesting for us to make such a deep change in an external codebase and the biggest lesson we learnt (again) was not technical but organisational : the communication between the people involved (developers and business actors) was critical to achieve this success.
Thank you again ListMinut for your trust and we wish you the best in France !