Control Vehicular: from Flask to Node.js on the same schema
A web system that reads fuel e-invoices issued under Colombia's DIAN and reconciles them with a distributor's fleet refueling records. My part was rewriting the backend from Python to Node.js, keeping the schema and the routes.
- Client
- Distribuciones Rosimar S.A.S.
- My part
- The backend migration and several features ported afterwards. 7 of the 24 commits1 are mine; the original system and its reconciliation logic are by another developer on the team.
- Period
- September and October 2026
- Stack
- Node.js 22, Express 5, MySQL with mysql2 (plain SQL), Nunjucks, node:test and supertest
- Code
- Private. controlvehicular.rosimar.com has restricted access.
Why migrate
The original application was written in Flask. The shared Hostinger hosting the client uses doesn't support Python frameworks, only on a VPS. It was rewritten, with one condition: don't change the behavior. Same MySQL schema, same routes, same screens. The only exception is the bugs in the original that were fixed along the way, listed below.
What was ported
- Authentication with a lockout after 5 failed attempts for 15 minutes, per user and per IP.
- CSRF on every POST, multipart forms included, checked before any data is processed.
- The parser for DIAN e-invoices: a ZIP with XML and PDF, with XML that uses namespaces.
- Reconciliation of fuel records against invoices by period, auditing, analysis, and Excel and CSV import.
- Plain, parameterized SQL with no ORM. Sessions and invoice files live in the database, not on disk.
- A module that reproduces Python's rounding so the totals match the previous system.
- 1
Request
Security headers and a session stored in the database.
- 2
CSRF and permissions
The CSRF token is checked before any data is processed; the role limits which routes each user sees.
- 3
Router
One router per module. The admin, auxiliar and jefe roles each see their own part.
- 4
SQL
mysql2 with parameterized queries, no ORM.
- 5
Template
Nunjucks, with a syntax almost identical to the Jinja2 the original used.
Bugs in the original that surfaced during the rewrite
Rewriting forces you to read every route. These problems came up and were fixed in the migration2:
- An open redirect on login.
- The bulk-linking route accepted GET without a CSRF token.
- The xlsx receipt import saved a list instead of an integer.
- A misplaced cell in the invoice export.
- The count of new findings was wrong in the initial analysis.
How it was tested
When the migration closed there were 48 tests3, unit and integration, with node:test and supertest against a real MySQL database, no mocks. Each test file creates and drops its own *_test database, and the helper refuses to run against any server that isn't localhost.
The Python code is kept in legacy/, as a reference and as a way back.
Limits
- There is no continuous integration. What reaches
mainis deployed straight to Hostinger, so the tests are run by hand before every push. - The tests need a local MySQL container.
- I didn't design the product. My part was the platform change and porting new features.
Sources for the figures
- 1
Commit authorship in fleet_control
git shortlog of rosimar1/fleet_control (private) · October 9, 2026
Another team developer 17 and Fabian Gonzalez (my git user) 7: 24 in total.
- 2
Bugs in the original fixed during the migration
commit d1cc1c2 message and test/integration/auth.test.js in rosimar1/fleet_control (private) · September 23, 2026
An open redirect on login (there is a test: next=//evil.com is ignored) and a bulk-linking route that accepted GET without a CSRF token.
- 3
Tests when the migration closed
rosimar1/fleet_control, commit d1cc1c2 (private) · September 23, 2026
35 calls to test() plus 14 cases generated by the permissions loop in test/integration/permissions.test.js. The commit message also says 48.