Let vacation years go back to the child's birth year
Settings only offered vacation-day rows from the current year forward, while analytics ran its balance series from the child's birth year. Every year in the gap had no vacation_days row, so the series assumed a full statutory 25 earned with no use and banked earned - floor of it forward — phantom saved days the user had no row to correct. Settings now offers editable rows back to the birth year, and a past year with no row assumes only the floor was earned, so it banks nothing. A past year the user did enter is untouched by that rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -843,11 +843,18 @@ class TestVacationYearBalanceSeries:
|
||||
"""Pure unit tests against `AnalyticsService._vacation_year_series` — the
|
||||
|
||||
FIFO carry-forward bank simulation documented on `ParentYearBalance`.
|
||||
|
||||
Every case here passes `current_year=years[0]` so its whole series is
|
||||
"current year or later" — these tests exercise the FIFO mechanics
|
||||
themselves, not the past-vs-current-year `earned` rule, which
|
||||
`TestPastYearEarnedRule` below covers on its own.
|
||||
"""
|
||||
|
||||
def test_no_rows_assumes_statutory_default_and_floor(self) -> None:
|
||||
years = [2025, 2026, 2027]
|
||||
result = AnalyticsService._vacation_year_series(years, {}, {}, floor=20)
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, {}, {}, floor=20, current_year=years[0]
|
||||
)
|
||||
|
||||
first = result[0]
|
||||
assert first.earned == 25 # rules.statutory_vacation_days_per_year default
|
||||
@@ -866,7 +873,9 @@ class TestVacationYearBalanceSeries:
|
||||
|
||||
def test_carry_chain_accumulates_across_years(self) -> None:
|
||||
years = [2025, 2026, 2027]
|
||||
result = AnalyticsService._vacation_year_series(years, {}, {}, floor=20)
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, {}, {}, floor=20, current_year=years[0]
|
||||
)
|
||||
|
||||
# Unused years keep saving 5/year (25 earned - 20 floor) and the
|
||||
# prior years' saved days carry forward untouched.
|
||||
@@ -881,7 +890,9 @@ class TestVacationYearBalanceSeries:
|
||||
# Exactly carry_years + 1 years: only 2020's own batch can possibly
|
||||
# have aged out by the last year in this series.
|
||||
years = list(range(2020, 2020 + carry_years + 1))
|
||||
result = AnalyticsService._vacation_year_series(years, {}, {}, floor=20)
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, {}, {}, floor=20, current_year=years[0]
|
||||
)
|
||||
by_year = {r.year: r for r in result}
|
||||
|
||||
# 2020's 5 saved days must not still be sitting in `carried_in`
|
||||
@@ -905,7 +916,9 @@ class TestVacationYearBalanceSeries:
|
||||
|
||||
carry_years = settings.rules.vacation_carry_forward_years
|
||||
years = list(range(2020, 2020 + carry_years + 3))
|
||||
result = AnalyticsService._vacation_year_series(years, {}, {}, floor=20)
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, {}, {}, floor=20, current_year=years[0]
|
||||
)
|
||||
by_year = {r.year: r for r in result}
|
||||
|
||||
for year in years[carry_years:]:
|
||||
@@ -931,7 +944,9 @@ class TestVacationYearBalanceSeries:
|
||||
saved_days_in=12,
|
||||
),
|
||||
}
|
||||
result = AnalyticsService._vacation_year_series(years, rows, {}, floor=20)
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, rows, {}, floor=20, current_year=years[0]
|
||||
)
|
||||
by_year = {r.year: r for r in result}
|
||||
|
||||
assert by_year[2025].carried_in == 0 # nothing entered/simulated yet
|
||||
@@ -954,7 +969,9 @@ class TestVacationYearBalanceSeries:
|
||||
saved_days_in=0,
|
||||
),
|
||||
}
|
||||
result = AnalyticsService._vacation_year_series(years, rows, {}, floor=20)
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, rows, {}, floor=20, current_year=years[0]
|
||||
)
|
||||
by_year = {r.year: r for r in result}
|
||||
|
||||
# 2025 saved 5 (25 - floor 20) with no usage; a zero saved_days_in on
|
||||
@@ -980,7 +997,9 @@ class TestVacationYearBalanceSeries:
|
||||
),
|
||||
}
|
||||
use_by_year = {2025: 15}
|
||||
result = AnalyticsService._vacation_year_series(years, rows, use_by_year, floor=20)
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, rows, use_by_year, floor=20, current_year=years[0]
|
||||
)
|
||||
balance = result[0]
|
||||
|
||||
assert balance.left == 15
|
||||
@@ -1000,7 +1019,9 @@ class TestVacationYearBalanceSeries:
|
||||
|
||||
carry_years = settings.rules.vacation_carry_forward_years
|
||||
years = list(range(2020, 2020 + carry_years + 1))
|
||||
result = AnalyticsService._vacation_year_series(years, {}, {}, floor=20)
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, {}, {}, floor=20, current_year=years[0]
|
||||
)
|
||||
last = result[-1]
|
||||
|
||||
assert last.expiring_age == 5
|
||||
@@ -1009,6 +1030,66 @@ class TestVacationYearBalanceSeries:
|
||||
assert last.expiring == last.expiring_age + last.expiring_over_cap
|
||||
|
||||
|
||||
class TestPastYearEarnedRule:
|
||||
"""`_vacation_year_series`'s `current_year` parameter — the fix for
|
||||
|
||||
Settings now offering editable rows back to the child's birth year
|
||||
(`computeVacationYearRange`). Before this, a year before `current_year`
|
||||
with no `vacation_days` row was indistinguishable from a future year:
|
||||
it assumed the full statutory 25 and banked `25 - floor` forward even
|
||||
though the row was never rendered for the user to correct — phantom
|
||||
"saved" vacation. See `ParentYearBalance`'s docstring.
|
||||
"""
|
||||
|
||||
def test_phantom_past_years_with_no_row_carry_nothing_forward(self) -> None:
|
||||
years = [2020, 2021, 2022]
|
||||
result = AnalyticsService._vacation_year_series(years, {}, {}, floor=20, current_year=2023)
|
||||
by_year = {r.year: r for r in result}
|
||||
|
||||
for year in years:
|
||||
assert by_year[year].earned == 20 # floor, not the statutory 25
|
||||
assert by_year[year].is_assumed is True
|
||||
assert by_year[year].carried_in == 0
|
||||
assert by_year[year].carried_out == 0
|
||||
assert [r.carried_in for r in result] == [0, 0, 0]
|
||||
|
||||
def test_entered_past_row_still_carries_its_surplus_forward(self) -> None:
|
||||
"""A past year the user genuinely entered data for is untouched by
|
||||
|
||||
the phantom-year rule above: its `total_days` still earns normally,
|
||||
and any real surplus it banks still carries into the next (also
|
||||
past) year — only a phantom (no-row) year banks nothing, never an
|
||||
entered one.
|
||||
"""
|
||||
from app.entities.vacation_days import VacationDays
|
||||
|
||||
years = [2020, 2021]
|
||||
rows = {
|
||||
2020: VacationDays(
|
||||
id=uuid4(),
|
||||
personal_data_uuid=uuid4(),
|
||||
workspace_id=uuid4(),
|
||||
year=2020,
|
||||
total_days=30,
|
||||
saved_days_in=0,
|
||||
),
|
||||
}
|
||||
result = AnalyticsService._vacation_year_series(
|
||||
years, rows, {}, floor=20, current_year=2023
|
||||
)
|
||||
by_year = {r.year: r for r in result}
|
||||
|
||||
assert by_year[2020].earned == 30 # the entered row, not the floor
|
||||
assert by_year[2020].is_assumed is False
|
||||
assert by_year[2020].carried_out == 10 # 30 - floor(20), genuinely saved
|
||||
|
||||
# 2021 has no row and is itself a past year, but 2020's REAL surplus
|
||||
# must still show up here, untouched.
|
||||
assert by_year[2021].earned == 20 # floor — 2021 itself has no row
|
||||
assert by_year[2021].carried_in == 10
|
||||
assert by_year[2021].carried_out == 10
|
||||
|
||||
|
||||
class TestParentalLeaveYearBalanceSeries:
|
||||
"""Pure unit tests against `AnalyticsService._pl_year_series`."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user