Credit days freed by the same sync before checking the balance
A sync deleted the rows a re-plan had dropped only after apply_proposed_days had already validated, so capacity being freed in the very same call was never counted. Shortening one period and lengthening another while near the pool cap read as pure net-new demand and was refused outright, writing nothing and losing the edit silently. apply_proposed_days now takes the keys it should delete, credits those rows back to their tier and to the dubbeldagar quota during the up-front checks, and performs the deletions in its own write phase. Validation still runs fully before any write, which is what SQLite's lack of a cross-request transaction requires.
This commit is contained in:
@@ -320,6 +320,105 @@ class TestSyncIdempotent:
|
||||
assert second.changes == []
|
||||
|
||||
|
||||
class TestSyncCreditsFreedCapacityWithinSameCall:
|
||||
"""Regression for the bug this change fixes: `sync_plan` used to delete
|
||||
|
||||
stale (shrunk-away) rows only AFTER `apply_proposed_days` had already
|
||||
validated and written the fresh batch, so a wish shrinking never
|
||||
credited the capacity it freed to a same-call growing wish -- a family
|
||||
near their sjukpenning cap who shortened one window and lengthened
|
||||
another would have the whole sync rejected with `insufficientBalance`
|
||||
even though the freed days more than covered the new demand.
|
||||
"""
|
||||
|
||||
async def test_shrinking_one_window_frees_capacity_for_a_new_one_in_the_same_sync(
|
||||
self,
|
||||
planning_service: PlanningService,
|
||||
planned_leave_repo: SQLitePlannedLeaveRepository,
|
||||
vacation_repo: SQLiteVacationDaysRepository,
|
||||
parental_leave_repo: SQLiteParentalLeaveRepository,
|
||||
leave_wish_repo: SQLiteLeaveWishRepository,
|
||||
seeded_ids: SeededIds,
|
||||
) -> None:
|
||||
ws, child, parent = (
|
||||
seeded_ids.workspace_id,
|
||||
seeded_ids.child_id,
|
||||
seeded_ids.personal_data_uuid,
|
||||
)
|
||||
# No vacation budget -> forces sjukpenning/lagsta funding, so this
|
||||
# test only has to reason about the sjukpenning pool.
|
||||
await vacation_repo.create(
|
||||
VacationDays(
|
||||
id=uuid4(), personal_data_uuid=parent, workspace_id=ws, year=2027, total_days=0
|
||||
)
|
||||
)
|
||||
await parental_leave_repo.create(
|
||||
_make_fetch(parent, ws, child, datetime(2027, 1, 1), sjuk_left=390, lagsta_left=90)
|
||||
)
|
||||
|
||||
summer = await leave_wish_repo.create(
|
||||
_make_wish(
|
||||
ws,
|
||||
child,
|
||||
name="Summer",
|
||||
window_start_month=6,
|
||||
window_start_day=1,
|
||||
window_end_month=6,
|
||||
window_end_day=30,
|
||||
target_weeks=4,
|
||||
)
|
||||
)
|
||||
|
||||
first = await planning_service.sync_plan(ws, today=date(2027, 1, 1))
|
||||
assert first.apply_result.accepted is True
|
||||
|
||||
summer_rows = await planned_leave_repo.list_by_workspace_child(
|
||||
ws, child, from_date=date(2027, 6, 1), to_date=date(2027, 6, 30)
|
||||
)
|
||||
sjuk_used = sum(1 for r in summer_rows if r.day_type == "sjukpenning")
|
||||
assert sjuk_used > 10, "expected the 4-week summer window to fund several sjukpenning days"
|
||||
|
||||
# Pin the balance to exactly what's already spent -> zero slack left,
|
||||
# simulating a family sitting right at their sjukpenning cap.
|
||||
await parental_leave_repo.create(
|
||||
_make_fetch(
|
||||
parent, ws, child, datetime(2027, 1, 2), sjuk_left=sjuk_used, lagsta_left=90
|
||||
)
|
||||
)
|
||||
|
||||
# Shrink summer to one week and add a smaller winter wish elsewhere.
|
||||
# The capacity freed by shrinking summer comfortably covers winter's
|
||||
# demand, but only if the freed days are credited within this call.
|
||||
summer.window_end_day = 7
|
||||
summer.target_weeks = 1
|
||||
await leave_wish_repo.update(summer.id, summer)
|
||||
await leave_wish_repo.create(
|
||||
_make_wish(
|
||||
ws,
|
||||
child,
|
||||
name="Winter",
|
||||
window_start_month=12,
|
||||
window_start_day=1,
|
||||
window_end_month=12,
|
||||
window_end_day=7,
|
||||
target_weeks=1,
|
||||
)
|
||||
)
|
||||
|
||||
second = await planning_service.sync_plan(ws, today=date(2027, 1, 1))
|
||||
|
||||
assert second.apply_result.accepted is True, second.apply_result.rejections
|
||||
assert not any(
|
||||
r.message_key == "planApply.rejection.insufficientBalance"
|
||||
for r in second.apply_result.rejections
|
||||
)
|
||||
|
||||
winter_rows = await planned_leave_repo.list_by_workspace_child(
|
||||
ws, child, from_date=date(2027, 12, 1), to_date=date(2027, 12, 7)
|
||||
)
|
||||
assert any(r.day_type == "sjukpenning" for r in winter_rows)
|
||||
|
||||
|
||||
class TestProposeStaysReadOnly:
|
||||
async def test_propose_never_writes_a_row(
|
||||
self,
|
||||
|
||||
Reference in New Issue
Block a user