Rework balances around per-parent days left and deduce the partner from FK
Manual entry now takes one parent's sjukpenningnivå, lägstanivå and dubbeldagar left (plus optional reserved days) and derives the rest in build_snapshot, shared with the scraper. The scraper also reads the family panel and exposes a partner deduction; with two parents it is saved as a 'deduced' snapshot (migration 002). Parent count is the single source of entitlement: one member gets 480 days and 96 savable at age 4, two get 240 and 48 each. Workspaces are capped at two parents. Also lands the in-flight squash of migrations 002–011 into the initial schema and the removal of planning_mode. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ Scraper endpoints (start / poll / cancel) return 501 when scraper_enabled is
|
||||
false. When enabled, they drive the FKScraperService session state machine.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
@@ -19,6 +20,7 @@ from app.dependencies import (
|
||||
ChildRepositoryDep,
|
||||
CurrentUserDep,
|
||||
ParentalLeaveRepositoryDep,
|
||||
PersonalDataRepositoryDep,
|
||||
WorkspaceScopeDep,
|
||||
)
|
||||
from app.entities.parental_leave import ParentalLeaveFetch
|
||||
@@ -27,10 +29,12 @@ from app.models.parental_leave import (
|
||||
ManualFetchRequest,
|
||||
ParentalLeaveFetchResponse,
|
||||
)
|
||||
from app.scrapers.fk_scraper import FKScraperService
|
||||
from app.services.fetch_data_service import ManualFetchService
|
||||
from app.scrapers.fk_scraper import FKScraperService, PartnerDeduction
|
||||
from app.services.fetch_data_service import ManualFetchService, persist_partner_deduction
|
||||
from app.utils.errors import ValidationError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/parental-leave", tags=["parental-leave"])
|
||||
scraper_router = APIRouter(prefix="/api/fetch-data", tags=["fetch-data"])
|
||||
|
||||
@@ -55,27 +59,27 @@ _scraper_service = FKScraperService()
|
||||
async def save_manual_entry(
|
||||
payload: ManualFetchRequest,
|
||||
repo: ParentalLeaveRepositoryDep,
|
||||
pd_repo: PersonalDataRepositoryDep,
|
||||
scope: WorkspaceScopeDep,
|
||||
) -> ParentalLeaveFetchResponse:
|
||||
scope.check_workspace(payload.workspace_id)
|
||||
await scope.check_member(payload.personal_data_uuid)
|
||||
await scope.check_child(payload.child_id)
|
||||
service = ManualFetchService(repo)
|
||||
# `pd_repo` lets the service default `total_days` off the workspace's
|
||||
# parent count when the caller omits it — see
|
||||
# `ManualFetchService.save_manual_entry`'s docstring.
|
||||
service = ManualFetchService(repo, pd_repo)
|
||||
try:
|
||||
fetch = await service.save_manual_entry(
|
||||
personal_data_uuid=payload.personal_data_uuid,
|
||||
workspace_id=payload.workspace_id,
|
||||
child_id=payload.child_id,
|
||||
total_days=payload.total_days,
|
||||
days_used=payload.days_used,
|
||||
reserved_days_total=payload.reserved_days_total,
|
||||
reserved_days_left=payload.reserved_days_left,
|
||||
transferable_days_left=payload.transferable_days_left,
|
||||
sjukpenning_left=payload.sjukpenning_left,
|
||||
lagsta_niva_left=payload.lagsta_niva_left,
|
||||
dubbeldagar_left=payload.dubbeldagar_left,
|
||||
sjukpenning_total=payload.sjukpenning_total,
|
||||
sjukpenning_used=payload.sjukpenning_used,
|
||||
lagsta_niva_total=payload.lagsta_niva_total,
|
||||
lagsta_niva_used=payload.lagsta_niva_used,
|
||||
reserved_days_left=payload.reserved_days_left,
|
||||
total_days=payload.total_days,
|
||||
source=payload.source,
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, str(exc)) from exc
|
||||
@@ -144,6 +148,7 @@ async def start_fetch(
|
||||
scope: WorkspaceScopeDep,
|
||||
child_repo: ChildRepositoryDep,
|
||||
parental_leave_repo: ParentalLeaveRepositoryDep,
|
||||
personal_data_repo: PersonalDataRepositoryDep,
|
||||
) -> _StartResponse:
|
||||
"""`workspace_id` is always the caller's own — `payload.workspace_id` is
|
||||
accepted on the wire (frontend compatibility) but never trusted; the
|
||||
@@ -169,11 +174,32 @@ async def start_fetch(
|
||||
)
|
||||
child_id = children[0].id
|
||||
|
||||
# Capture the repo for the background task. The connection lives for the app's
|
||||
# lifetime (app.state.db_connection), so using it from the background coroutine
|
||||
# is safe — aiosqlite serialises all operations through its internal queue.
|
||||
async def _persist(fetch: ParentalLeaveFetch) -> None:
|
||||
# Capture the repos for the background task. The connection lives for the
|
||||
# app's lifetime (app.state.db_connection), so using it from the
|
||||
# background coroutine is safe — aiosqlite serialises all operations
|
||||
# through its internal queue.
|
||||
async def _persist(fetch: ParentalLeaveFetch, partner: PartnerDeduction | None) -> None:
|
||||
await parental_leave_repo.create(fetch)
|
||||
if partner is not None:
|
||||
# Best-effort: the caller's own snapshot above already succeeded,
|
||||
# so a failure deducing/persisting the partner's snapshot must
|
||||
# never fail the whole scraper session (`persist_partner_deduction`
|
||||
# already swallows repo errors internally — this is a second,
|
||||
# redundant layer in case that ever changes).
|
||||
try:
|
||||
await persist_partner_deduction(
|
||||
personal_data_repo=personal_data_repo,
|
||||
parental_leave_repo=parental_leave_repo,
|
||||
workspace_id=workspace_id,
|
||||
child_id=child_id,
|
||||
caller_personal_data_uuid=personal_data_uuid,
|
||||
total_days=partner.total_days,
|
||||
sjukpenning_left=partner.sjukpenning_left,
|
||||
lagsta_niva_left=partner.lagsta_niva_left,
|
||||
dubbeldagar_left=partner.dubbeldagar_left,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("partner-deduction persist failed, session unaffected: %s", exc)
|
||||
|
||||
session_id = await _scraper_service.start_session(
|
||||
workspace_id=workspace_id,
|
||||
|
||||
Reference in New Issue
Block a user