money: count real Joint recurring items before the move instead of a flat €900/mo
Some checks failed
Deploy production / deploy (push) Has been cancelled

This commit is contained in:
2026-10-26 07:34:49 +00:00
parent e33afe5387
commit 2e8658bf33
3 changed files with 13 additions and 17 deletions

View File

@@ -177,8 +177,8 @@
Edits recalculate the chart right away and are not saved. Fixed for every scenario: contract Edits recalculate the chart right away and are not saved. Fixed for every scenario: contract
gross {cents(scenarioFacts.grossCents)}/mo until 30 Sep 2027, net salary and all costs from gross {cents(scenarioFacts.grossCents)}/mo until 30 Sep 2027, net salary and all costs from
the recurring list, cash buffer {cents(scenarioFacts.bufferCents)} then everything is invested, the recurring list, cash buffer {cents(scenarioFacts.bufferCents)} then everything is invested,
Joint contribution {cents(scenarioFacts.preMoveHouseholdCents)}/mo until the rent starts, Joint items from the recurring list in full until the rent starts, then your share, while your
your partner’s income pays her share, DUO {cents(scenarioFacts.duoCents)}/mo from {scenarioFacts.duoFrom} partner’s income pays hers, DUO {cents(scenarioFacts.duoCents)}/mo from {scenarioFacts.duoFrom}
to {scenarioFacts.duoUntil}. Prices, benefits, pension, the buffer and the salary plateau to {scenarioFacts.duoUntil}. Prices, benefits, pension, the buffer and the salary plateau
grow with inflation; spending grows at the spending rate for ten years, then with inflation. grow with inflation; spending grows at the spending rate for ten years, then with inflation.
Box 3 keeps taxing gains (losses carried forward). Salary bands (base per year, excl. 8% Box 3 keeps taxing gains (losses carried forward). Salary bands (base per year, excl. 8%

View File

@@ -98,23 +98,23 @@ describe('runScenario', () => {
}); });
it('pays salary on payday, sweeps the excess the day after and counts recurring income and costs', () => { it('pays salary on payday, sweeps the excess the day after and counts recurring income and costs', () => {
const points = runScenario(inputs, flat).points; const points = runScenario(inputs, flat).points;
// Without history the contract payday (the 25th) is used; €900 pre-move household is // Without history the contract payday (the 25th) is used. Before the move only the Joint
// spread at €30 a day in November. // recurring items count: €50 groceries (living with parents), spread at €1.67 a day.
expect(on(points, '2026-11-25').total - on(points, '2026-11-24').total).toBeCloseTo( expect(on(points, '2026-11-25').total - on(points, '2026-11-24').total).toBeCloseTo(
300000 + 51250 - 3000, 300000 + 51250 - 5000 / 30,
0 0
); );
// The benefit (paid on the 1st, a Sunday) moves to Monday. // The benefit (paid on the 1st, a Sunday) moves to Monday.
expect(on(points, '2026-11-02').total - on(points, '2026-11-01').total).toBeCloseTo( expect(on(points, '2026-11-02').total - on(points, '2026-11-01').total).toBeCloseTo(
10000 - 3000, 10000 - 5000 / 30,
0 0
); );
expect(on(points, '2026-11-25').cash).toBeGreaterThan(500000); expect(on(points, '2026-11-25').cash).toBeGreaterThan(500000);
expect(on(points, '2026-11-26').cash).toBe(500000); expect(on(points, '2026-11-26').cash).toBe(500000);
// Rest of October: 7/31 of the €900 pre-move household. November: 300k salary + 10k benefit // Rest of October: 7/31 of the €50 groceries. November: 300k salary + 10k benefit
// + 28k one-off + 13th month 3/12 − 3k gym − 90k pre-move household. // + 28k one-off + 13th month 3/12 − 3k gym − 5k groceries. No flat Joint contribution.
expect(on(points, '2026-11-30').total).toBeCloseTo( expect(on(points, '2026-11-30').total).toBeCloseTo(
350000 - (90000 * 7) / 31 + 300000 + 10000 + 28000 + 51250 - 3000 - 90000, 350000 - (5000 * 7) / 31 + 300000 + 10000 + 28000 + 51250 - 3000 - 5000,
0 0
); );
}); });
@@ -149,10 +149,7 @@ describe('runScenario', () => {
const points = runScenario(withHistory, flat).points; const points = runScenario(withHistory, flat).points;
// 21 November 2026 is a Saturday, so the salary lands on Friday the 20th. // 21 November 2026 is a Saturday, so the salary lands on Friday the 20th.
expect(on(points, '2026-11-20').total - on(points, '2026-11-19').total).toBeGreaterThan(300000); expect(on(points, '2026-11-20').total - on(points, '2026-11-19').total).toBeGreaterThan(300000);
expect(on(points, '2026-12-21').total - on(points, '2026-12-20').total).toBeCloseTo( expect(on(points, '2026-12-21').total - on(points, '2026-12-20').total).toBeCloseTo(300000, 0);
300000 - 90000 / 31,
0
);
}); });
it('orders the default scenarios from pessimistic to optimistic', () => { it('orders the default scenarios from pessimistic to optimistic', () => {
const [pessimistic, realistic, optimistic] = ( const [pessimistic, realistic, optimistic] = (

View File

@@ -220,8 +220,6 @@ export const scenarioFacts = {
sideTaxRate: 0.4, sideTaxRate: 0.4,
/** Cash buffer in today's euros (grows with inflation); everything above it is invested. */ /** Cash buffer in today's euros (grows with inflation); everything above it is invested. */
bufferCents: 500000, bufferCents: 500000,
/** Joint household contribution before the move, per month (Apr–Oct 2026 average). */
preMoveHouseholdCents: 90000,
/** DUO study-debt repayment, per month, for at most 35 years from 2029. */ /** DUO study-debt repayment, per month, for at most 35 years from 2029. */
duoFrom: 2029, duoFrom: 2029,
duoUntil: 2063, duoUntil: 2063,
@@ -691,8 +689,9 @@ export function runScenario(inputs: ScenarioInputs, params: ScenarioParams): Sce
} }
} }
if (first < moveMonth) { if (first < moveMonth) {
spending += facts.preMoveHouseholdCents * remaining; // Before the move the Joint account only pays the Joint items on the recurring list
spreadMonth(-facts.preMoveHouseholdCents); // (Netflix, groceries while living with my parents, …), in full: no partner income yet.
for (const item of inputs.household) addItem(item, spendGrowth, false);
} else { } else {
const share = pct(params.householdSharePercent) * spendGrowth; const share = pct(params.householdSharePercent) * spendGrowth;
const extra = params.householdExtraEuros * 100 * share; const extra = params.householdExtraEuros * 100 * share;