fiscal-es 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -2
  4. package/cases/SCHEMA.md +68 -0
  5. package/cases/irpf/ahorro/irpf-ahorro-2026-001.yaml +20 -0
  6. package/cases/irpf/ahorro/irpf-ahorro-2026-002.yaml +19 -0
  7. package/cases/irpf/ahorro/irpf-ahorro-2026-003.yaml +21 -0
  8. package/cases/irpf/ahorro/irpf-ahorro-2026-004.yaml +21 -0
  9. package/cases/irpf/ahorro/irpf-ahorro-2026-005.yaml +20 -0
  10. package/cases/irpf/ahorro/irpf-ahorro-2026-006.yaml +22 -0
  11. package/cases/irpf/ahorro/irpf-ahorro-2026-007.yaml +22 -0
  12. package/cases/irpf/ahorro/irpf-ahorro-2026-008.yaml +23 -0
  13. package/cases/irpf/ahorro/irpf-ahorro-2026-009.yaml +22 -0
  14. package/cases/irpf/ahorro/irpf-ahorro-2026-010.yaml +20 -0
  15. package/cases/irpf/ahorro/irpf-ahorro-2026-011.yaml +22 -0
  16. package/cases/irpf/ahorro/irpf-ahorro-2026-012.yaml +22 -0
  17. package/cases/irpf/ahorro/irpf-ahorro-2026-013.yaml +23 -0
  18. package/cases/irpf/ahorro/irpf-ahorro-2026-014.yaml +23 -0
  19. package/cases/irpf/ahorro/irpf-ahorro-2026-015.yaml +22 -0
  20. package/cases/irpf/ahorro/irpf-ahorro-2026-016.yaml +22 -0
  21. package/cases/irpf/ahorro/irpf-ahorro-2026-017.yaml +23 -0
  22. package/cases/irpf/ahorro/irpf-ahorro-2026-018.yaml +20 -0
  23. package/cases/irpf/ahorro/irpf-ahorro-2026-019.yaml +21 -0
  24. package/cases/irpf/ahorro/irpf-ahorro-2026-020.yaml +21 -0
  25. package/cases/irpf/ahorro/irpf-ahorro-2026-021.yaml +21 -0
  26. package/cases/irpf/ahorro/irpf-ahorro-2026-022.yaml +21 -0
  27. package/cases/irpf/ahorro/irpf-ahorro-2026-023.yaml +23 -0
  28. package/cases/irpf/ahorro/irpf-ahorro-2026-024.yaml +21 -0
  29. package/cases/irpf/ahorro/irpf-ahorro-2026-025.yaml +22 -0
  30. package/cases/irpf/ahorro/irpf-ahorro-2026-026.yaml +22 -0
  31. package/cases/irpf/compensacion/irpf-compensacion-2026-001.yaml +21 -0
  32. package/cases/irpf/compensacion/irpf-compensacion-2026-002.yaml +22 -0
  33. package/cases/irpf/compensacion/irpf-compensacion-2026-003.yaml +22 -0
  34. package/cases/irpf/compensacion/irpf-compensacion-2026-004.yaml +25 -0
  35. package/cases/irpf/compensacion/irpf-compensacion-2026-005.yaml +25 -0
  36. package/cases/irpf/compensacion/irpf-compensacion-2026-006.yaml +21 -0
  37. package/cases/irpf/compensacion/irpf-compensacion-2026-007.yaml +23 -0
  38. package/cases/irpf/compensacion/irpf-compensacion-2026-008.yaml +20 -0
  39. package/cases/irpf/compensacion/irpf-compensacion-2026-009.yaml +21 -0
  40. package/cases/irpf/compensacion/irpf-compensacion-2026-010.yaml +21 -0
  41. package/cases/irpf/compensacion/irpf-compensacion-2026-011.yaml +24 -0
  42. package/cases/irpf/compensacion/irpf-compensacion-2026-012.yaml +23 -0
  43. package/dist/errors.d.ts +13 -0
  44. package/dist/errors.js +20 -0
  45. package/dist/index.d.ts +7 -0
  46. package/dist/index.js +4 -0
  47. package/dist/money.d.ts +32 -0
  48. package/dist/money.js +65 -0
  49. package/dist/tables/2026.d.ts +28 -0
  50. package/dist/tables/2026.js +16 -0
  51. package/dist/tables/index.d.ts +6 -0
  52. package/dist/tables/index.js +14 -0
  53. package/dist/tables/tipos.d.ts +21 -0
  54. package/dist/tables/tipos.js +1 -0
  55. package/dist/tax/brackets.d.ts +7 -0
  56. package/dist/tax/brackets.js +18 -0
  57. package/dist/tax/offsetting.d.ts +10 -0
  58. package/dist/tax/offsetting.js +13 -0
  59. package/dist/tax/savings.d.ts +6 -0
  60. package/dist/tax/savings.js +44 -0
  61. package/dist/tax/tipos.d.ts +23 -0
  62. package/dist/tax/tipos.js +1 -0
  63. package/package.json +53 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ **Breaking: all public identifiers renamed to English** (ADR-009). The computation is unchanged: every verified
6
+ case in `cases/` gives the same output as in 0.1.0. The YAML keys of the cases do not change.
7
+
8
+ Error messages are now in English.
9
+
10
+ | 0.1.0 | 1.0.0 |
11
+ | ----- | ----- |
12
+ | `Centimos` | `Cents` |
13
+ | `Bp`, `bp`, `parseEuros`, `formatEuros` | unchanged |
14
+ | `CERO` | `ZERO` |
15
+ | `sumar` | `add` |
16
+ | `restar` | `subtract` |
17
+ | `max0` | `clampZero` |
18
+ | `porBp` | `timesBp` |
19
+ | `redondearCentimos` | `roundCents` |
20
+ | `calcularAhorro` | `computeSavingsTax` |
21
+ | `EntradaAhorro` | `SavingsInput` |
22
+ | `EntradaAhorro.ejercicio` | `SavingsInput.taxYear` |
23
+ | `EntradaAhorro.gananciasPatrimoniales` | `SavingsInput.capitalGains` |
24
+ | `EntradaAhorro.perdidasPatrimoniales` | `SavingsInput.capitalLosses` |
25
+ | `EntradaAhorro.rendimientosCapitalMobiliario` | `SavingsInput.investmentIncome` |
26
+ | `ResultadoAhorro` | `SavingsResult` |
27
+ | `ResultadoAhorro.baseLiquidableAhorro` | `SavingsResult.savingsTaxableBase` |
28
+ | `ResultadoAhorro.cuotaEstatal` | `SavingsResult.stateTax` |
29
+ | `ResultadoAhorro.cuotaAutonomica` | `SavingsResult.regionalTax` |
30
+ | `ResultadoAhorro.cuotaAhorro` | `SavingsResult.totalTax` |
31
+ | `getTabla` | `getTaxTable` |
32
+ | `ejerciciosDisponibles` | `availableTaxYears` |
33
+ | `TablaIRPF` | `TaxTable` |
34
+ | `TablaIRPF.ejercicio` | `TaxTable.taxYear` |
35
+ | `TablaIRPF.fuente` / `fuente.verificado` | `TaxTable.source` / `source.verifiedOn` |
36
+ | `TablaIRPF.escalaAhorro` | `TaxTable.brackets` |
37
+ | `TramoAhorro` | `TaxBracket` |
38
+ | `TramoAhorro.hasta` / `estatalBp` / `autonomicoBp` | `TaxBracket.upTo` / `stateBp` / `regionalBp` |
39
+ | `EjercicioSinTabla` (`.ejercicio`, `name`) | `UnknownTaxYear` (`.taxYear`, `name`) |
40
+ | `EntradaInvalida` (`.campo`, `.valor`, `name`) | `InvalidInput` (`.field`, `.value`, `name`) |
41
+
42
+ ## 0.1.0
43
+
44
+ First release.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Miguel Vidal Bellido
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,117 @@
1
- # Temporary Holding Version
1
+ # fiscal-es
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Spanish personal income tax (IRPF) on the savings base (Spain, common territory): integrates capital gains, capital
4
+ losses and investment income for one tax year and applies the state and regional scales separately.
5
+
6
+ TypeScript, ESM, no runtime dependencies, pure functions. Amounts in integer cents (`Cents`) and rates in integer
7
+ basis points (`Bp`); never floating point.
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ npm install fiscal-es
13
+ ```
14
+
15
+ ESM only (`import`); there is no CommonJS build.
16
+
17
+ ## Usage
18
+
19
+ ### Computing the savings tax
20
+
21
+ ```js
22
+ import { computeSavingsTax, formatEuros, parseEuros } from "fiscal-es";
23
+
24
+ const r = computeSavingsTax({
25
+ taxYear: 2026,
26
+ capitalGains: parseEuros("10000.00"),
27
+ capitalLosses: parseEuros("3000.00"),
28
+ investmentIncome: parseEuros("0.00"),
29
+ });
30
+ // r = { savingsTaxableBase: 700000, stateTax: 67500, regionalTax: 67500, totalTax: 135000 }
31
+ formatEuros(r.totalTax); // "1350.00"
32
+ ```
33
+
34
+ `parseEuros` only accepts the format `"-1234.56"` (decimal point, two decimals); anything else throws
35
+ `SyntaxError`. Input that makes no sense (non-integer amounts, negative gains or losses) throws `InvalidInput`;
36
+ a tax year without a table throws `UnknownTaxYear`.
37
+
38
+ ### Available tax years
39
+
40
+ ```js
41
+ import { availableTaxYears, getTaxTable } from "fiscal-es";
42
+
43
+ for (const taxYear of availableTaxYears()) {
44
+ console.log(taxYear, getTaxTable(taxYear).source.verifiedOn); // 2026 "2026-10-25"
45
+ }
46
+ ```
47
+
48
+ ## Range and errors
49
+
50
+ The computation is exact for amounts up to ≈ 9 000 million euros per operation: with rates up to 10 000 bp, the
51
+ intermediate product cents × basis points fits in `Number.MAX_SAFE_INTEGER`. Above that, any operation whose result
52
+ or intermediate product leaves the safe integer range throws `RangeError`.
53
+
54
+ - `SyntaxError`: `parseEuros` receives a format other than `"-1234.56"` (see [Usage](#usage)).
55
+ - `InvalidInput`: `computeSavingsTax` receives an amount that is not an integer number of cents (`NaN`, a fraction
56
+ of a cent), negative gains or losses, or a non-integer tax year.
57
+ - `UnknownTaxYear`: `computeSavingsTax` receives a tax year without a table.
58
+ - `RangeError`: `parseEuros`, `add`, `subtract`, `timesBp` or `roundCents` receive or produce a value outside the
59
+ safe integer range.
60
+
61
+ ## Supported tax years
62
+
63
+ | Tax year | Scale verified on |
64
+ | -------- | ----------------- |
65
+ | 2026 | 2026-10-25 |
66
+
67
+ ## `cases/`: the executable spec
68
+
69
+ Each YAML file in `cases/` is a case with its input, the expected output to the cent, the legal reference and the
70
+ date on which a person verified it. The package's test suite runs all of them against the engine; a change to the
71
+ engine that alters the output of a verified case is a major version change. The format is described in
72
+ `cases/SCHEMA.md`. The cases are written in Spanish, with Spanish keys (`ganancias_patrimoniales`, `cuota_ahorro`…),
73
+ because they are checked against the Spanish tax return manual; [Terminology](#terminology) maps them to the API.
74
+
75
+ ## Terminology
76
+
77
+ | Identifier | Spanish term | LIRPF |
78
+ | ---------- | ------------ | ----- |
79
+ | `computeSavingsTax` | cuota del ahorro | arts. 49, 50.2, 66.1 y 76 |
80
+ | `SavingsInput` / `SavingsResult` | entrada / resultado del ahorro | |
81
+ | `taxYear` | ejercicio | |
82
+ | `capitalGains` (`ganancias_patrimoniales`) | ganancias patrimoniales | art. 33 |
83
+ | `capitalLosses` (`perdidas_patrimoniales`) | pérdidas patrimoniales | art. 33 |
84
+ | `investmentIncome` (`rendimientos_capital_mobiliario`) | rendimientos del capital mobiliario | art. 25 |
85
+ | `savingsTaxableBase` (`base_liquidable_ahorro`) | base liquidable del ahorro | arts. 49 y 50.2 |
86
+ | `stateTax` (`cuota_estatal`) | cuota estatal | art. 66.1 |
87
+ | `regionalTax` (`cuota_autonomica`) | cuota autonómica | art. 76 |
88
+ | `totalTax` (`cuota_ahorro`) | cuota del ahorro (estatal + autonómica) | |
89
+ | `TaxTable` / `getTaxTable` / `availableTaxYears` | tabla normativa del ejercicio / ejercicios disponibles | |
90
+ | `TaxBracket` (`upTo`, `stateBp`, `regionalBp`) | tramo de la escala del ahorro (hasta, tipo estatal, tipo autonómico) | arts. 66.1 y 76 |
91
+ | `UnknownTaxYear` | ejercicio sin tabla | |
92
+ | `InvalidInput` | entrada inválida | |
93
+ | `Cents` / `Bp` | céntimos / puntos básicos | |
94
+ | `add` / `subtract` / `clampZero` / `ZERO` | sumar / restar / acotar a cero / cero | |
95
+ | `timesBp` / `roundCents` | céntimos × puntos básicos / redondeo al céntimo | |
96
+
97
+ ## Known limitations
98
+
99
+ - No cross-offsetting: a negative balance of gains and losses does not offset positive investment income, nor the
100
+ other way round (art. 49.1 LIRPF). The negative balance simply is not taxed.
101
+ - No carry-forward of negative balances to the following four tax years (art. 49.2 LIRPF).
102
+ - No foral regimes (Navarra, País Vasco).
103
+ - Savings base only: does not compute the general base, personal allowances or deductions.
104
+
105
+ ## Development in the monorepo
106
+
107
+ `exports` has a custom condition, `fiscal-es-src`, pointing to `src/` so the monorepo app can use the engine without
108
+ building it; external consumers do not enable it and get `dist/`.
109
+
110
+ ## Disclaimer
111
+
112
+ A calculation tool. Not tax or financial advice. Check your situation against current legislation or with a
113
+ professional.
114
+
115
+ ## License
116
+
117
+ MIT
@@ -0,0 +1,68 @@
1
+ # Casos fiscales — esquema y reglas
2
+
3
+ Los ficheros `cases/**/*.yaml` son **la spec del motor**. A la vez: test, documentación y contrato público.
4
+ Un único test genérico (`tests/cases.test.ts`) los carga todos y ejecuta cada uno contra el motor.
5
+ Comparación exacta en céntimos. No hay tolerancias.
6
+
7
+ ## Formato
8
+
9
+ ```yaml
10
+ id: irpf-ahorro-2026-002 # único; <área>-<tema>-<ejercicio>-<nnn>
11
+ title: Ganancia que cruza el primer tramo de la base del ahorro
12
+ ejercicio: 2026
13
+ estado: rojo # rojo | verde. Solo pasa a verde tras revisión humana del resultado.
14
+ normativa:
15
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
16
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
17
+ verificado: null # fecha ISO cuando una persona ha contrastado expected con la fuente; null si no
18
+ input:
19
+ ganancias_patrimoniales: "8000.00"
20
+ perdidas_patrimoniales: "0.00"
21
+ rendimientos_capital_mobiliario: "0.00"
22
+ expected:
23
+ base_liquidable_ahorro: "8000.00"
24
+ cuota_estatal: "780.00" # 6000·9,5 % + 2000·10,5 %
25
+ cuota_autonomica: "780.00"
26
+ cuota_ahorro: "1560.00"
27
+ notas: >
28
+ Texto libre opcional: cómo se ha calculado a mano, dudas, referencias al manual de Renta.
29
+ ```
30
+
31
+ ## Reglas de validación (el loader las aplica; un caso inválido hace fallar la suite)
32
+
33
+ 1. Todo importe (`input.*` y `expected.*`) es un **string** decimal con exactamente dos decimales y punto.
34
+ Un número YAML (`8000`, `8000.0`) para dinero es un error. Motivo: ADR-002.
35
+ 2. `id` único en todo `cases/`. El nombre del fichero es `<id>.yaml`.
36
+ 3. `ejercicio` tiene que tener tabla en `src/tablas/`. Si no, el caso falla con un mensaje que lo diga.
37
+ 4. `normativa` tiene al menos una entrada. Sin cita, no hay caso.
38
+ 5. `estado: verde` exige `verificado` no nulo. El test lo comprueba: un caso verde sin verificación es un fallo.
39
+ 6. Un cambio en `expected` de un caso existente exige cambiar `verificado` a `null` o a una fecha nueva.
40
+ (Esto lo comprueba la revisión humana y `fiscal-reviewer`, no el test.)
41
+
42
+ ## Cómo se calcula (para quien escribe casos a mano)
43
+
44
+ - La base liquidable del ahorro = saldo de rendimientos del capital mobiliario + saldo de (ganancias − pérdidas).
45
+ En v1 cada saldo se integra por separado y **un saldo negativo se trata como cero**: ni compensa al otro saldo
46
+ (el 25 % cruzado del art. 49.1) ni se arrastra a ejercicios siguientes. Es una simplificación deliberada
47
+ (ADR-005); cuando se implemente la compensación cruzada se añadirán casos nuevos, no se cambiarán estos.
48
+ Consecuencias: pérdidas mayores que ganancias → saldo cero; rendimientos negativos → saldo cero.
49
+ - Cuota estatal y cuota autonómica se calculan **por separado**, cada una aplicando su escala por tramos a la
50
+ misma base, y cada una se redondea al céntimo. `cuota_ahorro` es la suma de las dos ya redondeadas.
51
+ Esto importa: 1.234,56 € × 9,5 % = 117,2832 → 117,28; dos veces = 234,56, mientras que 1.234,56 × 19 %
52
+ = 234,5664 → 234,57. El motor replica el cálculo de Renta, no el atajo.
53
+
54
+ ## Qué NO va aquí
55
+
56
+ - Casos de UI, de formato o de URL. Eso es Vitest normal en `apps/web`.
57
+ - Simulaciones completas de N años. Eso es `packages/fiscal-es/tests/simulacion.test.ts` cuando exista
58
+ (y usa el motor caso a caso).
59
+
60
+ ## Convención de carpetas
61
+
62
+ ```
63
+ cases/
64
+ irpf/
65
+ ahorro/ # escala de la base del ahorro
66
+ compensacion/
67
+ README-por-area.md (opcional)
68
+ ```
@@ -0,0 +1,20 @@
1
+ id: irpf-ahorro-2026-001
2
+ title: Ganancia dentro del primer tramo de la base del ahorro
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "5000.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "5000.00"
15
+ cuota_estatal: "475.00"
16
+ cuota_autonomica: "475.00"
17
+ cuota_ahorro: "950.00"
18
+ notas: >
19
+ 5.000 × 9,5 % = 475,00 estatal; igual autonómico. Total 19 %.
20
+ Pendiente de verificar la escala vigente del ejercicio 2026 contra el manual de Renta.
@@ -0,0 +1,19 @@
1
+ id: irpf-ahorro-2026-002
2
+ title: Ganancia que cruza el primer tramo de la base del ahorro
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "8000.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "8000.00"
15
+ cuota_estatal: "780.00"
16
+ cuota_autonomica: "780.00"
17
+ cuota_ahorro: "1560.00"
18
+ notas: >
19
+ Estatal: 6.000 × 9,5 % = 570,00 + 2.000 × 10,5 % = 210,00 → 780,00. Autonómico idéntico.
@@ -0,0 +1,21 @@
1
+ id: irpf-ahorro-2026-003
2
+ title: Base que recorre todos los tramos de la escala del ahorro
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "350000.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "350000.00"
15
+ cuota_estatal: "43440.00"
16
+ cuota_autonomica: "43440.00"
17
+ cuota_ahorro: "86880.00"
18
+ notas: >
19
+ Tipos combinados: 19 % hasta 6.000; 21 % hasta 50.000; 23 % hasta 200.000; 27 % hasta 300.000; 30 % resto.
20
+ Combinado: 1.140 + 9.240 + 34.500 + 27.000 + 15.000 = 86.880,00. Mitad estatal, mitad autonómico.
21
+ Este caso existe para pillar errores de umbral (¿el tramo cierra en 50.000,00 o en 50.000,01?).
@@ -0,0 +1,21 @@
1
+ id: irpf-ahorro-2026-004
2
+ title: Redondeo por separado de cuota estatal y autonómica (intereses de cuenta remunerada)
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 25.2 (intereses como rendimiento del capital mobiliario)"
7
+ - ref: "Ley 35/2006 IRPF, art. 66.1 y art. 76"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "0.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "1234.56"
13
+ expected:
14
+ base_liquidable_ahorro: "1234.56"
15
+ cuota_estatal: "117.28"
16
+ cuota_autonomica: "117.28"
17
+ cuota_ahorro: "234.56"
18
+ notas: >
19
+ 1.234,56 × 9,5 % = 117,2832 → 117,28 (cada cuota por separado). Suma 234,56.
20
+ El atajo 1.234,56 × 19 % = 234,5664 → 234,57 es INCORRECTO. Este caso existe para detectar
21
+ implementaciones que calculan con el tipo combinado. Verificar la regla de redondeo con el manual de Renta.
@@ -0,0 +1,20 @@
1
+ id: irpf-ahorro-2026-005
2
+ title: Base del ahorro cero
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "0.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "0.00"
15
+ cuota_estatal: "0.00"
16
+ cuota_autonomica: "0.00"
17
+ cuota_ahorro: "0.00"
18
+ notas: >
19
+ Sin rentas del ahorro. Base 0 → cuotas 0,00. Detecta implementaciones que fallan con base vacía
20
+ (bucle de tramos que no arranca, división por cero, "-0.00" en la salida).
@@ -0,0 +1,22 @@
1
+ id: irpf-ahorro-2026-006
2
+ title: Base exactamente en el umbral superior del primer tramo (6.000,00)
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "6000.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "6000.00"
15
+ cuota_estatal: "570.00"
16
+ cuota_autonomica: "570.00"
17
+ cuota_ahorro: "1140.00"
18
+ notas: >
19
+ Estatal: 6.000,00 × 9,5 % = 570,00. Autonómico idéntico. Total 1.140,00.
20
+ Todo dentro del primer tramo: ni un céntimo puede ir al 10,5 %. Si el motor trata `hasta` como
21
+ exclusivo y cruza al segundo tramo, la diferencia aquí es 0 céntimos (ver 007); el caso sirve sobre
22
+ todo para detectar que se aplique el tipo marginal a toda la base (6.000 × 10,5 % = 630,00).
@@ -0,0 +1,22 @@
1
+ id: irpf-ahorro-2026-007
2
+ title: Base un céntimo por encima del primer umbral (6.000,01)
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "6000.01"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "6000.01"
15
+ cuota_estatal: "570.00"
16
+ cuota_autonomica: "570.00"
17
+ cuota_ahorro: "1140.00"
18
+ notas: >
19
+ Estatal en céntimos·bp: 600.000 × 950 = 570.000.000 + 1 × 1.050 = 1.050 → 570.001.050 / 10.000
20
+ = 57.000,105 c → 57.000 c = 570,00. Autonómico idéntico. Total 1.140,00.
21
+ Errores que detecta: el tipo marginal sobre toda la base (6.000,01 × 10,5 % = 630,00), y el redondeo
22
+ hacia arriba o por exceso del tramo de 0,01 € (daría 570,01).
@@ -0,0 +1,23 @@
1
+ id: irpf-ahorro-2026-008
2
+ title: Medio céntimo exacto en cada cuota (redondeo mitad alejándose de cero, no bancario ni truncado)
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 25.2 (intereses como rendimiento del capital mobiliario)"
7
+ - ref: "Ley 35/2006 IRPF, art. 66.1 y art. 76"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "0.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "3.00"
13
+ expected:
14
+ base_liquidable_ahorro: "3.00"
15
+ cuota_estatal: "0.29"
16
+ cuota_autonomica: "0.29"
17
+ cuota_ahorro: "0.58"
18
+ notas: >
19
+ Estatal: 300 c × 950 bp = 285.000 → 28,5 c → 29 c = 0,29 (mitad hacia arriba). Autonómico idéntico. Total 0,58.
20
+ Resultados erróneos que distingue: truncado → 0,28 + 0,28 = 0,56; redondeo bancario (half-even, 28,5 → 28)
21
+ → 0,56; tipo combinado 3,00 × 19 % = 0,57.
22
+ Duda normativa: confirmar con el manual de Renta o con el programa Renta Web que la AEAT redondea cada
23
+ cuota al céntimo con la mitad hacia arriba (ADR-002 lo asume).
@@ -0,0 +1,22 @@
1
+ id: irpf-ahorro-2026-009
2
+ title: Base con céntimos en el segundo tramo; el redondeo por cuota separada difiere del combinado
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "7777.77"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "7777.77"
15
+ cuota_estatal: "756.67"
16
+ cuota_autonomica: "756.67"
17
+ cuota_ahorro: "1513.34"
18
+ notas: >
19
+ Estatal en céntimos·bp: 600.000 × 950 = 570.000.000; 177.777 × 1.050 = 186.665.850; suma 756.665.850
20
+ / 10.000 = 75.666,585 c → 75.667 c = 756,67. Autonómico idéntico. Total 1.513,34.
21
+ Atajo combinado: 600.000 × 1.900 + 177.777 × 2.100 = 1.513.331.700 → 151.333,17 c → 1.513,33 (incorrecto).
22
+ Este caso es como el 004, pero cruza un tramo y lleva céntimos en la base.
@@ -0,0 +1,20 @@
1
+ id: irpf-ahorro-2026-010
2
+ title: Base de un céntimo; la cuota redondea a cero
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 25.2 (intereses como rendimiento del capital mobiliario)"
7
+ - ref: "Ley 35/2006 IRPF, art. 66.1 y art. 76"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "0.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.01"
13
+ expected:
14
+ base_liquidable_ahorro: "0.01"
15
+ cuota_estatal: "0.00"
16
+ cuota_autonomica: "0.00"
17
+ cuota_ahorro: "0.00"
18
+ notas: >
19
+ 1 c × 950 bp = 950 → 0,095 c → 0 c. Autonómico idéntico. Total 0,00.
20
+ Detecta el redondeo hacia arriba (ceil), que daría 0,01 + 0,01.
@@ -0,0 +1,22 @@
1
+ id: irpf-ahorro-2026-011
2
+ title: Base un céntimo por encima del segundo umbral (50.000,01)
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "50000.01"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "50000.01"
15
+ cuota_estatal: "5190.00"
16
+ cuota_autonomica: "5190.00"
17
+ cuota_ahorro: "10380.00"
18
+ notas: >
19
+ Estatal en céntimos·bp: 600.000 × 950 = 570.000.000; 4.400.000 × 1.050 = 4.620.000.000; 1 × 1.150 = 1.150;
20
+ suma 5.190.001.150 / 10.000 = 519.000,115 c → 519.000 c = 5.190,00. Autonómico idéntico. Total 10.380,00.
21
+ Detecta `hasta` tratado como exclusivo en el segundo tramo y el marginal aplicado a toda la base
22
+ (50.000,01 × 11,5 % = 5.750,00).
@@ -0,0 +1,22 @@
1
+ id: irpf-ahorro-2026-012
2
+ title: Base un céntimo por encima del último umbral (300.000,01), entra en el tramo sin límite
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "300000.01"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "300000.01"
15
+ cuota_estatal: "35940.00"
16
+ cuota_autonomica: "35940.00"
17
+ cuota_ahorro: "71880.00"
18
+ notas: >
19
+ Estatal en céntimos·bp: 600.000 × 950 = 570.000.000; 4.400.000 × 1.050 = 4.620.000.000;
20
+ 15.000.000 × 1.150 = 17.250.000.000; 10.000.000 × 1.350 = 13.500.000.000; 1 × 1.500 = 1.500;
21
+ suma 35.940.001.500 / 10.000 = 3.594.000,15 c → 3.594.000 c = 35.940,00. Autonómico idéntico. Total 71.880,00.
22
+ Detecta un tramo `hasta: null` que no se aplica, o que se aplica desde 0, y el `hasta` exclusivo en el último umbral.
@@ -0,0 +1,23 @@
1
+ id: irpf-ahorro-2026-013
2
+ title: Medio céntimo exacto en el segundo tramo (redondeo mitad hacia arriba y cuotas separadas)
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "6001.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "6001.00"
15
+ cuota_estatal: "570.11"
16
+ cuota_autonomica: "570.11"
17
+ cuota_ahorro: "1140.22"
18
+ notas: >
19
+ Estatal en céntimos·bp: 600.000 × 950 = 570.000.000 + 100 × 1.050 = 105.000 → 570.105.000 / 10.000
20
+ = 57.010,5 c → 57.011 c = 570,11 (mitad hacia arriba). Autonómico idéntico. Total 1.140,22.
21
+ Errores que detecta: half-even o truncado (57.010 → 570,10; total 1.140,20); tipo combinado
22
+ (600.000 × 1.900 + 100 × 2.100 = 1.140.210.000 → 1.140,21). El caso 008 cubre el medio céntimo solo
23
+ en el primer tramo; este lo cubre en un tramo que no es el primero.
@@ -0,0 +1,23 @@
1
+ id: irpf-ahorro-2026-014
2
+ title: Medio céntimo exacto en el último tramo (sin límite superior)
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "300000.30"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "300000.30"
15
+ cuota_estatal: "35940.05"
16
+ cuota_autonomica: "35940.05"
17
+ cuota_ahorro: "71880.10"
18
+ notas: >
19
+ Estatal en céntimos·bp: tramos completos 570.000.000 + 4.620.000.000 + 17.250.000.000 + 13.500.000.000
20
+ = 35.940.000.000; + 30 × 1.500 = 45.000 → 35.940.045.000 / 10.000 = 3.594.004,5 c → 3.594.005 c
21
+ = 35.940,05. Autonómico idéntico. Total 71.880,10.
22
+ Errores que detecta: half-even o truncado (3.594.004 → 71.880,08); tipo combinado
23
+ (7.188.000.000... + 30 × 3.000 = 90.000 → 71.880,09).
@@ -0,0 +1,22 @@
1
+ id: irpf-ahorro-2026-015
2
+ title: Base exactamente en el umbral de 200.000,00
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "200000.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "200000.00"
15
+ cuota_estatal: "22440.00"
16
+ cuota_autonomica: "22440.00"
17
+ cuota_ahorro: "44880.00"
18
+ notas: >
19
+ Estatal: 600.000 × 950 + 4.400.000 × 1.050 + 15.000.000 × 1.150 = 22.440.000.000 / 10.000
20
+ = 2.244.000 c = 22.440,00. Autonómico idéntico. Total 44.880,00 (1.140 + 9.240 + 34.500).
21
+ Ningún caso existente toca el umbral de 200.000 (el 003 lo cruza de largo). Detecta el tipo marginal
22
+ aplicado a toda la base (200.000 × 11,5 % = 23.000,00) y un tercer tramo mal cerrado.
@@ -0,0 +1,22 @@
1
+ id: irpf-ahorro-2026-016
2
+ title: Un céntimo por encima del umbral de 200.000,00
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "200000.01"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "200000.01"
15
+ cuota_estatal: "22440.00"
16
+ cuota_autonomica: "22440.00"
17
+ cuota_ahorro: "44880.00"
18
+ notas: >
19
+ Estatal: 22.440.000.000 + 1 × 1.350 = 22.440.001.350 / 10.000 = 2.244.000,135 c → 2.244.000 c = 22.440,00.
20
+ Autonómico idéntico. Total 44.880,00.
21
+ Detecta el marginal sobre toda la base (200.000,01 × 13,5 % = 27.000,00) y el redondeo hacia arriba
22
+ del céntimo que entra en el cuarto tramo (daría 22.440,01).
@@ -0,0 +1,23 @@
1
+ id: irpf-ahorro-2026-017
2
+ title: Base muy grande dentro del rango seguro (1.000 millones de euros)
3
+ ejercicio: 2026
4
+ estado: verde
5
+ normativa:
6
+ - ref: "Ley 35/2006 IRPF, art. 66.1 (gravamen estatal base del ahorro)"
7
+ - ref: "Ley 35/2006 IRPF, art. 76 (gravamen autonómico base del ahorro)"
8
+ verificado: "2026-10-25"
9
+ input:
10
+ ganancias_patrimoniales: "1000000000.00"
11
+ perdidas_patrimoniales: "0.00"
12
+ rendimientos_capital_mobiliario: "0.00"
13
+ expected:
14
+ base_liquidable_ahorro: "1000000000.00"
15
+ cuota_estatal: "149990940.00"
16
+ cuota_autonomica: "149990940.00"
17
+ cuota_ahorro: "299981880.00"
18
+ notas: >
19
+ Estatal en céntimos·bp: tramos completos 35.940.000.000 + (100.000.000.000 − 30.000.000) × 1.500
20
+ = 149.955.000.000.000 → suma 149.990.940.000.000 (≈ 1,5 × 10¹⁴, dentro de MAX_SAFE_INTEGER)
21
+ / 10.000 = 14.999.094.000 c = 149.990.940,00. Autonómico idéntico. Total 299.981.880,00
22
+ (= 71.880 + 999.700.000 × 30 %). Comprueba que el acumulado de toda la escala no pierde precisión
23
+ ni lanza un falso RangeError por debajo del límite de ADR-002.