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: 'raiseMonth', label: 'Raise month', 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: 'sideEndEuros', label: 'Side income at end (gross)', unit: '€/mo' }
|
||||
]
|
||||
|
||||
@@ -77,8 +77,18 @@ describe('runScenario', () => {
|
||||
const result = runScenario(inputs, flat);
|
||||
const november = result.points[1];
|
||||
expect(november.cash).toBe(500000);
|
||||
// 300k salary + 10k benefit + 28k one-off − 3k gym − 90k pre-move household.
|
||||
expect(november.total).toBeCloseTo(350000 + 300000 + 10000 + 28000 - 3000 - 90000, 0);
|
||||
// 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 + 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', () => {
|
||||
const [pessimistic, realistic, optimistic] = (
|
||||
|
||||
@@ -33,7 +33,7 @@ export type ScenarioParams = {
|
||||
raisePercent: number;
|
||||
raiseMonth: 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;
|
||||
/** Side income, gross euros per month, ramping linearly to the end value. */
|
||||
sideStartEuros: number;
|
||||
@@ -95,7 +95,7 @@ export const defaultScenarios: Record<ScenarioId, ScenarioParams> = {
|
||||
raisePercent: 3.5,
|
||||
raiseMonth: 10,
|
||||
raiseFromYear: 2028,
|
||||
bonusPercentOfTarget: 50,
|
||||
bonusPercentOfTarget: 100,
|
||||
sideStartEuros: 250,
|
||||
sideEndEuros: 500,
|
||||
spendGrowthPercent: 2.5,
|
||||
@@ -141,10 +141,19 @@ export const scenarioFacts = {
|
||||
contractEnd: '2027-09-30',
|
||||
/** Extra net per extra gross euro: about 50% marginal tax after 2.65% pension. */
|
||||
marginalNet: (1 - 0.0265) * 0.5,
|
||||
/** Holiday pay (8%) and 13th month accrue monthly; paid half net in May and December. */
|
||||
holidayPayRate: 0.08,
|
||||
/** Target bonus as a share of yearly gross (12.96 months), paid half net in September. */
|
||||
bonusTarget: 0.05,
|
||||
/**
|
||||
* Contract extras, net per full year at the contract gross (scaled with later pay): holiday
|
||||
* pay paid in May and the 13th month in November, both accrued per month worked. The first
|
||||
* 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,
|
||||
bufferCents: 500000,
|
||||
/** 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 invested = inputs.investedCents;
|
||||
let gross = baseGross;
|
||||
let accruedHoliday = baseGross * facts.holidayPayRate;
|
||||
let accrued13 = baseGross / 12;
|
||||
let accruedHoliday = (facts.holidayNetYearCents / 12) * facts.holidayMonthsAccrued;
|
||||
let accrued13 = (facts.thirteenthNetYearCents / 12) * facts.thirteenthMonthsAccrued;
|
||||
let investedAtYearStart = invested;
|
||||
let contributedThisYear = 0;
|
||||
let box3Due = 0;
|
||||
@@ -357,25 +366,29 @@ export function runScenario(inputs: ScenarioInputs, params: ScenarioParams): Sce
|
||||
gross *= 1 + pct(params.raisePercent);
|
||||
let income = employed ? netFromGross(gross) : params.gapNetEuros * 100;
|
||||
if (employed) {
|
||||
accruedHoliday += gross * facts.holidayPayRate;
|
||||
accrued13 += gross / 12;
|
||||
accruedHoliday += (facts.holidayNetYearCents / 12) * (gross / baseGross);
|
||||
accrued13 += (facts.thirteenthNetYearCents / 12) * (gross / baseGross);
|
||||
}
|
||||
if (month === 5) {
|
||||
income += accruedHoliday * 0.5;
|
||||
income += accruedHoliday;
|
||||
accruedHoliday = 0;
|
||||
}
|
||||
if (month === 12) {
|
||||
income += accrued13 * 0.5;
|
||||
if (month === 11) {
|
||||
income += accrued13;
|
||||
accrued13 = 0;
|
||||
}
|
||||
// A job change pays out what is still accrued when the contract ends.
|
||||
if (params.gapMonths > 0 && first === addMonths(facts.contractEnd, 0)) {
|
||||
income += (accruedHoliday + accrued13) * 0.5;
|
||||
income += accruedHoliday + accrued13;
|
||||
accruedHoliday = 0;
|
||||
accrued13 = 0;
|
||||
}
|
||||
if (month === 9 && first >= addMonths(facts.contractEnd, 0) && employed)
|
||||
income += pct(params.bonusPercentOfTarget) * facts.bonusTarget * gross * 12.96 * 0.5;
|
||||
if (month === 8 && year >= facts.firstBonusYear && employed)
|
||||
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 sideGross = params.sideStartEuros + (params.sideEndEuros - params.sideStartEuros) * ramp;
|
||||
income += sideGross * 100 * (1 - facts.sideTaxRate);
|
||||
|
||||
Reference in New Issue
Block a user