Skip to content
Anderson F. González
Back to work

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. 1

    Request

    Security headers and a session stored in the database.

  2. 2

    CSRF and permissions

    The CSRF token is checked before any data is processed; the role limits which routes each user sees.

  3. 3

    Router

    One router per module. The admin, auxiliar and jefe roles each see their own part.

  4. 4

    SQL

    mysql2 with parameterized queries, no ORM.

  5. 5

    Template

    Nunjucks, with a syntax almost identical to the Jinja2 the original used.

Each module (authentication, catalogs, invoices, fuel records, reconciliation, reports) has its own router.

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 main is 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. 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. 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. 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.