Back to blog
El panel del optimizador fiscal foral: el ahorro identificado del ejercicio en grande, la barra del crédito vitalicio de vivienda agotado a 36.000 €, y el desglose por vivienda, EPSV e inversiones, con el pie que recuerda que todo es una proyección del mismo log de eventos y que la herramienta es orientativa.

A simulator for Bizkaia's foral income tax

LabEvent sourcingTaxationReact

The spreadsheets people use to plan their taxes all share the same flaw: they can't be undone. You try paying the mortgage down early, the cell changes, and you no longer know what the number was before or why you had put it there. When decisions also step on each other —what you contribute to a pension plan changes the base that decides whether you can deduct your home— the spreadsheet stops being a tool and becomes a source of errors with pretty formatting.

The experiment was to build that tool for Bizkaia's 2026 foral tax rules with a self-imposed constraint: that state can never be modified, only derived. Event sourcing in a homegrown calculator, to see whether it holds up or whether it's artillery to kill flies.

The log is the truth; the screen, an opinion

There is no stored state. There is a list of facts that happened —you set this field, you added this position, you recorded this sale— and the state is what comes out of walking it from zero. Two functions, and neither holds any mystery:

// El estado nunca se muta directamente: es la proyección de un log de eventos
// { id, ts, type, payload }. Reproducir el log desde el estado inicial
// reconstruye exactamente el estado actual (y los perfiles guardan el log).
export function reducirEvento(estado, ev) {
  const p = ev.payload || {};
  switch (ev.type) {
    case "CAMPO_FIJADO":
      return { ...estado, [p.campo]: p.valor };

    case "POSICION_ANADIDA":
      return { ...estado, cartera: [...(estado.cartera || []), p] };

    // …
  }
}

export const proyectar = (eventos, inicial) =>
  (eventos || []).reduce(reducirEvento, inicial);

From that come three free things that in the mutable-state version would have taken work. Undoing is removing the last event. Saving a profile is saving the log, not the result, so when you open it, it replays in full and you know why every number is what it is. And comparing two scenarios is comparing two lists, not two spreadsheets.

What did force some thinking was undoing the big decisions. When you apply a divestment plan, one event isn't emitted: many are, one per sale. Undoing it by truncating the history is tempting and wrong, because it erases facts that did happen. It's reverted with compensating events: new facts that cancel the earlier ones and leave the full trail that there was a plan and that it was withdrawn.

The 3% rule forces splitting across years

The case that justifies everything above is divestment. The foral rule allows certain sales to be taxed at a special 3% rate as long as the year's transfer value is below €10,000. Above that, the general regime applies. With a portfolio of a certain size, the question stops being “how much do I pay?” and becomes “in what order and over how many years do I sell?”.

The planner splits it: whatever falls under the general regime is sold in full in the first year, and whatever takes the 3% route is sliced into tranches that never reach the limit, rolling over to the next year when the room runs out. It's a pure domain use case —a portfolio goes in, the facts that would need recording come out— and that's why it can be tested without opening the application.

What is official and what is my model

This is the part I'm most satisfied with, and it isn't code. A tax calculator written by someone who isn't a tax advisor has a problem of honesty before one of accuracy: there are figures that come from the official gazette and figures that come from my assumptions, and mixing them is what turns a useful tool into a trap.

The validation suite separates the two things explicitly. Sixty-three of the checks are contrasted against the published source: the 2026 savings and general tax scales, bracket by bracket against the tax-amount column; the worked housing-deduction example from the Foral Treasury's guide, reproduced to the cent; the contribution limits and the 3% boundary already described.

Others are marked as model: social security contributions, the earned-income reduction, the freelancer simplification. There is no official figure to reproduce there, so they're validated for internal coherence and stated to be approximations. The whole suite is 155 checks and they all pass; what matters isn't the number, but that when you read a result you know which side of that line it comes from.

155checks, all green
63against the published official source
322lines in the calculation engine
0mutable state

Was event sourcing worth it?

For a single-user calculator, with no concurrency and no real audit, the honest answer is that it's more than the problem asked for. A mutable state with an undo history would have taken less time.

It paid off elsewhere. What the events fixed wasn't undoing: it was being able to answer “where does this number come from?”. When the result depends on four decisions that affect each other, and the user comes back three weeks later, the difference between an opaque state and a readable log is the difference between trusting the tool or not. That's the property I'm taking with me, and it's the same one I ask for in an enterprise system where someone is going to ask why what was charged was charged.

Limitations, and an important one

  • This is not tax advice. It's an outreach tool built by reading the foral rules. The current regulation always prevails, and a wrong interpretation of mine doesn't stop being wrong because the suite is green.
  • A single territory and a single tax year. Bizkaia, 2026. The scales are written into the engine; taking it to another foral territory isn't configuring it, it's reading another regulation all over again.
  • It expires. Every budget law moves rates and limits. Without someone reviewing the sources every year, this ages on its own — and an outdated tax calculator is worse than having none.

The pattern does travel, even if the content expires: complex regulation turned into pure functions, a suite that distinguishes what the gazette says from what the author assumes, and a decision log instead of a state that gets overwritten. It works just as well for computing a payroll, a regulated tariff or a tiered commission. The full simulator is public and is in the GitHub repository.

Zetesis-Labs/optimizador-fiscal-bizkaia

Simulator for Bizkaia's foral income tax: housing credit, the 3% rule, EPSV and portfolio.

JavaScript

More from the lab