money scenarios: contract timing for salary extras (13th month in Nov, holiday pay in May, bonus in Aug, net estimates from the contract)
All checks were successful
Deploy production / deploy (push) Successful in 2m32s
All checks were successful
Deploy production / deploy (push) Successful in 2m32s
This commit is contained in:
@@ -52,7 +52,7 @@
|
|||||||
{ key: 'raisePercent', label: 'Yearly raise', unit: '%' },
|
{ key: 'raisePercent', label: 'Yearly raise', unit: '%' },
|
||||||
{ key: 'raiseMonth', label: 'Raise month', unit: '' },
|
{ key: 'raiseMonth', label: 'Raise month', unit: '' },
|
||||||
{ key: 'raiseFromYear', label: 'Raises from', unit: '' },
|
{ key: 'raiseFromYear', label: 'Raises from', unit: '' },
|
||||||
{ key: 'bonusPercentOfTarget', label: 'Bonus paid of target', unit: '%' },
|
{ key: 'bonusPercentOfTarget', label: 'Bonus paid of estimate', unit: '%' },
|
||||||
{ key: 'sideStartEuros', label: 'Side income now (gross)', unit: '€/mo' },
|
{ key: 'sideStartEuros', label: 'Side income now (gross)', unit: '€/mo' },
|
||||||
{ key: 'sideEndEuros', label: 'Side income at end (gross)', unit: '€/mo' }
|
{ key: 'sideEndEuros', label: 'Side income at end (gross)', unit: '€/mo' }
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -77,8 +77,18 @@ describe('runScenario', () => {
|
|||||||
const result = runScenario(inputs, flat);
|
const result = runScenario(inputs, flat);
|
||||||
const november = result.points[1];
|
const november = result.points[1];
|
||||||
expect(november.cash).toBe(500000);
|
expect(november.cash).toBe(500000);
|
||||||
// 300k salary + 10k benefit + 28k one-off − 3k gym − 90k pre-move household.
|
// 300k salary + 10k benefit + 28k one-off + 13th month 3/12 − 3k gym − 90k pre-move household.
|
||||||
expect(november.total).toBeCloseTo(350000 + 300000 + 10000 + 28000 - 3000 - 90000, 0);
|
expect(november.total).toBeCloseTo(350000 + 300000 + 10000 + 28000 + 51250 - 3000 - 90000, 0);
|
||||||
|
});
|
||||||
|
it('pays contract extras in May (holiday pay) and August (bonus)', () => {
|
||||||
|
const delta = (points: { total: number }[], index: number) =>
|
||||||
|
points[index].total - points[index - 1].total;
|
||||||
|
const flatRun = runScenario(inputs, flat).points;
|
||||||
|
// Index 7 is May 2027: holiday pay 8/12 of €1,950.
|
||||||
|
expect(delta(flatRun, 7) - delta(flatRun, 6)).toBeCloseTo(130000, 0);
|
||||||
|
const bonus = runScenario(inputs, { ...flat, bonusPercentOfTarget: 100 }).points;
|
||||||
|
// Index 10 is August 2027: the first bonus, €850.
|
||||||
|
expect(delta(bonus, 10) - delta(flatRun, 10)).toBeCloseTo(85000, 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] = (
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export type ScenarioParams = {
|
|||||||
raisePercent: number;
|
raisePercent: number;
|
||||||
raiseMonth: number;
|
raiseMonth: number;
|
||||||
raiseFromYear: number;
|
raiseFromYear: number;
|
||||||
/** Share of the 5% target bonus actually paid each September, 0–100. */
|
/** Share of the contract's bonus estimate actually paid each August, 0–100. */
|
||||||
bonusPercentOfTarget: number;
|
bonusPercentOfTarget: number;
|
||||||
/** Side income, gross euros per month, ramping linearly to the end value. */
|
/** Side income, gross euros per month, ramping linearly to the end value. */
|
||||||
sideStartEuros: number;
|
sideStartEuros: number;
|
||||||
@@ -95,7 +95,7 @@ export const defaultScenarios: Record<ScenarioId, ScenarioParams> = {
|
|||||||
raisePercent: 3.5,
|
raisePercent: 3.5,
|
||||||
raiseMonth: 10,
|
raiseMonth: 10,
|
||||||
raiseFromYear: 2028,
|
raiseFromYear: 2028,
|
||||||
bonusPercentOfTarget: 50,
|
bonusPercentOfTarget: 100,
|
||||||
sideStartEuros: 250,
|
sideStartEuros: 250,
|
||||||
sideEndEuros: 500,
|
sideEndEuros: 500,
|
||||||
spendGrowthPercent: 2.5,
|
spendGrowthPercent: 2.5,
|
||||||
@@ -141,10 +141,19 @@ export const scenarioFacts = {
|
|||||||
contractEnd: '2027-09-30',
|
contractEnd: '2027-09-30',
|
||||||
/** Extra net per extra gross euro: about 50% marginal tax after 2.65% pension. */
|
/** Extra net per extra gross euro: about 50% marginal tax after 2.65% pension. */
|
||||||
marginalNet: (1 - 0.0265) * 0.5,
|
marginalNet: (1 - 0.0265) * 0.5,
|
||||||
/** Holiday pay (8%) and 13th month accrue monthly; paid half net in May and December. */
|
/**
|
||||||
holidayPayRate: 0.08,
|
* Contract extras, net per full year at the contract gross (scaled with later pay): holiday
|
||||||
/** Target bonus as a share of yearly gross (12.96 months), paid half net in September. */
|
* pay paid in May and the 13th month in November, both accrued per month worked. The first
|
||||||
bonusTarget: 0.05,
|
* ones are pro rata (13th month 3/12 in Nov 2026, holiday pay 8/12 in May 2027). The bonus
|
||||||
|
* is paid in August after the fiscal year, only while employed: once €850 (FY27), then €1,300.
|
||||||
|
*/
|
||||||
|
holidayNetYearCents: 195000,
|
||||||
|
thirteenthNetYearCents: 205000,
|
||||||
|
holidayMonthsAccrued: 1,
|
||||||
|
thirteenthMonthsAccrued: 2,
|
||||||
|
firstBonusCents: 85000,
|
||||||
|
firstBonusYear: 2027,
|
||||||
|
bonusNetYearCents: 130000,
|
||||||
sideTaxRate: 0.4,
|
sideTaxRate: 0.4,
|
||||||
bufferCents: 500000,
|
bufferCents: 500000,
|
||||||
/** Joint household contribution before the move, per month (Apr–Oct 2026 average). */
|
/** Joint household contribution before the move, per month (Apr–Oct 2026 average). */
|
||||||
@@ -301,8 +310,8 @@ export function runScenario(inputs: ScenarioInputs, params: ScenarioParams): Sce
|
|||||||
let cash = inputs.spendableCents;
|
let cash = inputs.spendableCents;
|
||||||
let invested = inputs.investedCents;
|
let invested = inputs.investedCents;
|
||||||
let gross = baseGross;
|
let gross = baseGross;
|
||||||
let accruedHoliday = baseGross * facts.holidayPayRate;
|
let accruedHoliday = (facts.holidayNetYearCents / 12) * facts.holidayMonthsAccrued;
|
||||||
let accrued13 = baseGross / 12;
|
let accrued13 = (facts.thirteenthNetYearCents / 12) * facts.thirteenthMonthsAccrued;
|
||||||
let investedAtYearStart = invested;
|
let investedAtYearStart = invested;
|
||||||
let contributedThisYear = 0;
|
let contributedThisYear = 0;
|
||||||
let box3Due = 0;
|
let box3Due = 0;
|
||||||
@@ -357,25 +366,29 @@ export function runScenario(inputs: ScenarioInputs, params: ScenarioParams): Sce
|
|||||||
gross *= 1 + pct(params.raisePercent);
|
gross *= 1 + pct(params.raisePercent);
|
||||||
let income = employed ? netFromGross(gross) : params.gapNetEuros * 100;
|
let income = employed ? netFromGross(gross) : params.gapNetEuros * 100;
|
||||||
if (employed) {
|
if (employed) {
|
||||||
accruedHoliday += gross * facts.holidayPayRate;
|
accruedHoliday += (facts.holidayNetYearCents / 12) * (gross / baseGross);
|
||||||
accrued13 += gross / 12;
|
accrued13 += (facts.thirteenthNetYearCents / 12) * (gross / baseGross);
|
||||||
}
|
}
|
||||||
if (month === 5) {
|
if (month === 5) {
|
||||||
income += accruedHoliday * 0.5;
|
income += accruedHoliday;
|
||||||
accruedHoliday = 0;
|
accruedHoliday = 0;
|
||||||
}
|
}
|
||||||
if (month === 12) {
|
if (month === 11) {
|
||||||
income += accrued13 * 0.5;
|
income += accrued13;
|
||||||
accrued13 = 0;
|
accrued13 = 0;
|
||||||
}
|
}
|
||||||
// A job change pays out what is still accrued when the contract ends.
|
// A job change pays out what is still accrued when the contract ends.
|
||||||
if (params.gapMonths > 0 && first === addMonths(facts.contractEnd, 0)) {
|
if (params.gapMonths > 0 && first === addMonths(facts.contractEnd, 0)) {
|
||||||
income += (accruedHoliday + accrued13) * 0.5;
|
income += accruedHoliday + accrued13;
|
||||||
accruedHoliday = 0;
|
accruedHoliday = 0;
|
||||||
accrued13 = 0;
|
accrued13 = 0;
|
||||||
}
|
}
|
||||||
if (month === 9 && first >= addMonths(facts.contractEnd, 0) && employed)
|
if (month === 8 && year >= facts.firstBonusYear && employed)
|
||||||
income += pct(params.bonusPercentOfTarget) * facts.bonusTarget * gross * 12.96 * 0.5;
|
income +=
|
||||||
|
pct(params.bonusPercentOfTarget) *
|
||||||
|
(year === facts.firstBonusYear
|
||||||
|
? facts.firstBonusCents
|
||||||
|
: facts.bonusNetYearCents * (gross / baseGross));
|
||||||
const ramp = Math.min(1, Math.max(0, (index - 3) / Math.max(1, totalMonths - 4)));
|
const ramp = Math.min(1, Math.max(0, (index - 3) / Math.max(1, totalMonths - 4)));
|
||||||
const sideGross = params.sideStartEuros + (params.sideEndEuros - params.sideStartEuros) * ramp;
|
const sideGross = params.sideStartEuros + (params.sideEndEuros - params.sideStartEuros) * ramp;
|
||||||
income += sideGross * 100 * (1 - facts.sideTaxRate);
|
income += sideGross * 100 * (1 - facts.sideTaxRate);
|
||||||
|
|||||||
Reference in New Issue
Block a user