Context
An accounting category was the classification used to group charged items in financial reports. It was also used during later checks, so it was not just a technical detail in the database. When the category on an item was wrong, the reports were wrong too.
The original implementation did not store the category on the item as a stable value. Whenever the system needed to read it, for example while generating a report, it calculated it from the current configuration and other related data.
Problem
The biggest problem was that historical data was not stable enough. A user could generate a report for the same past period twice at different times and get different results, because the configuration used to calculate the accounting category had changed in the meantime.
For accounting data, that is a fundamental difference. Current configuration should affect future or still-open items, but it should not retroactively change the meaning of items that already belong to the past or are part of a closed account.
Clarifying the calculation
Before we could change when the accounting category was stored or recalculated, we first had to understand and simplify the calculation itself. It was spread across multiple places: different item types were tangled together, and the logic for one item type could be scattered across several classes or helper methods at once.
The refactoring aimed to give each item type one clear place where its accounting category was calculated. That made it possible to walk through the algorithm step by step and, when something was wrong, distinguish whether the problem was in the setup, the input data, or the calculation itself.
Part of the work was also documenting the algorithm by item type. The documentation was mainly for support, which could not see into the code but needed to verify why a specific item received a specific category.
Snapshotting and recalculation
The next step was to start storing the accounting category directly on the item. That meant a report no longer had to return to the current configuration every time and recalculate a value that should have been stable for historical data.
But it was not a simple “store it and never change it again” model. If data changed in a way that should legitimately affect future or still-open items, the category had to be recalculated.
The goal was to preserve both needs at the same time: future items had to react to relevant configuration changes, while historical reports should not be rewritten just because the configuration had changed since then.
Migration and reports
Because snapshotting was introduced only after a large number of items already existed in the system, historical data had to be evaluated retroactively. I prepared a migrator that could run in two modes: dry run only generated a report, while fix mode applied corrections.
I used dry run to generate exports for more than 5000 hotels. The output was a CSV/Excel report listing items where the stored accounting category did not match. It included item identification, the current stored value, the newly calculated correct value, and the data that made the calculation verifiable.
The hardest technical part was performance and operations. Calculating the category required loading a lot of hotel configuration data. If one batch contained items from thousands of different hotels, the system had to load configuration and related records for each of them. When processing one hotel at a time, the same configuration data could be reused for a large part of that hotel’s items. That is why I prepared orchestration around the migration per hotel: one job iterated through hotels and started separate processing for each hotel’s items.
I then handed the correction mode to the support team together with instructions. Support could use the same tool for hotels that requested a correction: first verify the impact in the report, and then run fix mode if needed.
Outcome
The work took roughly four months and we brought it to completion. The accounting category started being stored directly on items, so historical reports no longer had to depend on the current configuration at the moment of generation.
Clarifying the whole calculation was just as important to me. When a question appeared about why an item had a certain category, it became possible to walk through the specific algorithm for that item type, verify the setup, and distinguish a configuration issue from a code issue.
Alongside the change itself, we also created an operational tool: a reporting and correction job that support could use for specific hotels. That made the migration more than a one-off script. It became a safer process where the impact could be checked first and the data corrected only afterwards.