Skip to content

Project

Hotel management system

Role
Backend, financial module and data protection
Context
Client product
Stack
  • typescript
  • nestjs
  • prisma
  • postgresql
  • react
  • lgpd

o200k_base · 199 tokens · what the model reads

A commercial product in production, with the financial module modelled as a ledger: a mistake becomes a reversal, never an UPDATE.

The problem

Hotel management concentrates three things that rarely coexist well: daily operations that cannot stop, a financial module where a one cent error is a real error, and guest personal data under Brazil data protection law.

It is the kind of system where the hard part is no specific technology, but consistency between modules that have to agree about the same fact. The nightly rate posted at check in is the same fact the daily cash close settles, the monthly report sums, and someone will have to explain three months later when the accountant asks. If those four numbers disagree, there is no such thing as a small bug.

The solution

A full hotel PMS, from booking and stay to cash, inventory and tax documents, with the financial module modelled as an append only ledger and the audit trail written in the same transaction as the mutation.

How a query moves through the system

  1. 01

    Check in

    the stay begins and becomes a financial fact.

  2. 02

    Charge

    the nightly rate is posted as an entry, and entries are not edited.

  3. 03

    Payment

    enters as its own movement, without altering the charge it settled.

  4. 04

    Cash close

    the close sums movements, never a balance someone edits.

  5. 05

    Audit

    each step writes the trail in the same transaction that produced it.

Engineering decisions

  1. 01

    The financial module is a ledger, not CRUD.

    Alternative considered
    UPDATE and DELETE on the financial tables, with every row always reflecting current state.
    Why
    History that can be rewritten is not history. The movement, charge and payment tables are append only: a wrong entry is not corrected, it is reversed by a new movement pointing at the original. Money is Decimal from the database to the front end, formatted through a helper, because float in monetary arithmetic is not a precision choice, it is a deferred error.
    What it cost
    More code and duller screens. Every reversal shows the context of the original movement before confirming, and no correction is a single click.
  2. 02

    The audit trail is written in the same transaction as the mutation.

    Alternative considered
    Writing the trail after the mutation, in a try/catch or on a queue.
    Why
    An audit trail written afterwards is an audit trail that disappears in exactly the incident where you need it. What it records goes through an allowlist, IDs and flags, never the whole DTO: the audit log is the easiest place to leak personal data without anyone noticing.
    What it cost
    The transaction gets longer, and every new field forces an explicit decision about whether it belongs on the allowlist before shipping.
  3. 03

    Personal data is an architectural constraint, not a policy page.

    Alternative considered
    Storing the file in the database and handling data protection at the consent and privacy policy layer.
    Why
    Consent covers collection. It does not cover the data that escapes through a log, a toast or a URL that stayed valid for six months. Binaries live in object storage and the database keeps only metadata and a reference; the signed URL is short lived, generated on demand and only after the permission check, never precomputed. Email, phone, token and URL never appear in logs, console or query strings, and the audit screen has redaction of its own.
    What it cost
    No debugging shortcuts. Tracing one guest problem means going through the identifier, not through the detail the person used to complain.
  4. 04

    Modular monolith, by explicit decision.

    Alternative considered
    Splitting into services from the start, with a queue between the modules that talk to each other.
    Why
    One client, one operation, a small team. Distributed complexity here would be a daily cost in exchange for a hypothetical benefit. The boundary that matters at this size is the module: controller, service and repository, with the controller forbidden from touching Prisma. When the product justifies the split, it is already drawn.
    What it cost
    Scaling means scaling the whole process, and an error in one module takes the others down with it. The split, when it comes, will be done with the system in production.

Outcome

System in production, sold to a client and running the daily operation of a hotel.

Back to projects