← Files VeraARCHIVED FILE

modules/previdenza-inps/scripts/reconcile_contributions.py

17 KB · Oct 2, 2026 · 00:29 UTC

↓ Download file

#!/usr/bin/env python3
"""Recalculate reviewer-approved contribution recipes with exact Decimal arithmetic."""

from __future__ import annotations

import argparse
import csv
import hashlib
import json
import logging
import sys
from datetime import datetime, timezone
from decimal import (
    ROUND_DOWN,
    ROUND_HALF_EVEN,
    ROUND_HALF_UP,
    ROUND_UP,
    Decimal,
    DivisionByZero,
    InvalidOperation,
)
from pathlib import Path
from typing import Any

from case_core import (
    ensure_safe_output_dir,
    mark_private_file,
    prepare_private_directory,
    write_json,
)

__all__ = ["evaluate_recipes", "main"]

LOGGER = logging.getLogger(__name__)
PLUGIN_ROOT = Path(__file__).resolve().parents[1]
for _vendor_root in (
    PLUGIN_ROOT / "vendor" / "modules",
    PLUGIN_ROOT.parent.parent / "vendor" / "modules",
    PLUGIN_ROOT.parent / "_shared" / "vendor" / "modules",
):
    if (_vendor_root / "vera_assurance").is_dir():
        if str(_vendor_root) not in sys.path:
            sys.path.insert(0, str(_vendor_root))
        break

from vera_assurance import (  # noqa: E402
    AssuranceContractError,
    load_client_engagement_context_file,
)

OPERATIONS = {"add", "divide", "multiply", "subtract"}
ROUNDING_MODES = {
    "ROUND_DOWN": ROUND_DOWN,
    "ROUND_HALF_EVEN": ROUND_HALF_EVEN,
    "ROUND_HALF_UP": ROUND_HALF_UP,
    "ROUND_UP": ROUND_UP,
}


def _utc_now() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat()


def _run_relative_path(path: Path, run_root: Path) -> str:
    """Return a portable path anchored to the current workflow run."""

    resolved_root = run_root.expanduser().resolve(strict=True)
    resolved_path = path.expanduser().resolve(strict=True)
    try:
        return resolved_path.relative_to(resolved_root).as_posix()
    except ValueError as exc:
        raise ValueError("workflow path is outside the current run") from exc


def _valid_approval_datetime(value: Any) -> bool:
    if not isinstance(value, str):
        return False
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError:
        return False
    return parsed.tzinfo is not None


def _load_object(path: Path) -> dict[str, Any]:
    payload = json.loads(path.read_text(encoding="utf-8"))
    if not isinstance(payload, dict):
        raise ValueError(f"JSON root must be an object: {path}")
    return payload


def _file_sha256(path: Path) -> str:
    return hashlib.sha256(path.read_bytes()).hexdigest()


def _claim_map(claims: dict[str, Any]) -> dict[str, dict[str, Any]]:
    result: dict[str, dict[str, Any]] = {}
    for index, claim in enumerate(claims.get("claims", []), start=1):
        if not isinstance(claim, dict):
            continue
        claim_id = str(claim.get("claim_id") or f"CL-{index:03d}")
        result[claim_id] = claim
    return result


def _fact_map(records: dict[str, Any]) -> dict[str, dict[str, Any]]:
    return {
        str(fact.get("fact_id")): fact
        for fact in records.get("facts", [])
        if isinstance(fact, dict) and fact.get("fact_id")
    }


def _decimal(value: Any, field: str) -> Decimal:
    try:
        return Decimal(str(value))
    except (InvalidOperation, ValueError) as exc:
        raise ValueError(f"{field} must be a decimal-compatible value") from exc


def _calculate(operation: str, left: Decimal, right: Decimal) -> Decimal:
    if operation == "add":
        return left + right
    if operation == "subtract":
        return left - right
    if operation == "multiply":
        return left * right
    if operation == "divide":
        return left / right
    raise ValueError(f"unsupported operation: {operation}")


def _validate_operand(
    operand: dict[str, Any],
    *,
    fact_map: dict[str, dict[str, Any]],
    claim_map: dict[str, dict[str, Any]],
    recipe_id: str,
) -> Decimal:
    operand_id = str(operand.get("id", "")).strip()
    if not operand_id:
        raise ValueError(f"{recipe_id}: operand id is required")
    fact_refs = [str(value) for value in operand.get("source_fact_ids", [])]
    claim_refs = [str(value) for value in operand.get("source_claim_ids", [])]
    if not fact_refs and not claim_refs:
        raise ValueError(f"{recipe_id}.{operand_id}: provenance is required")
    unknown_facts = sorted(set(fact_refs) - set(fact_map))
    unknown_claims = sorted(set(claim_refs) - set(claim_map))
    if unknown_facts:
        raise ValueError(
            f"{recipe_id}.{operand_id}: unknown fact refs {', '.join(unknown_facts)}"
        )
    if unknown_claims:
        raise ValueError(
            f"{recipe_id}.{operand_id}: unknown claim refs {', '.join(unknown_claims)}"
        )
    for fact_id in fact_refs:
        if fact_map[fact_id].get("review_status") != "confirmed":
            raise ValueError(
                f"{recipe_id}.{operand_id}: fact {fact_id} is not confirmed"
            )
    for claim_id in claim_refs:
        claim = claim_map[claim_id]
        if claim.get("verdict") != "supported":
            raise ValueError(
                f"{recipe_id}.{operand_id}: claim {claim_id} is not fully supported"
            )
    value = _decimal(operand.get("value"), f"{recipe_id}.{operand_id}.value")
    if len(fact_refs) == 1:
        source_fact = fact_map[fact_refs[0]]
        if source_fact.get("value_type") in {"amount", "number", "percentage"}:
            fact_value = _decimal(
                source_fact.get("value"), f"{recipe_id}.{operand_id}.source_fact_value"
            )
            if fact_value != value:
                raise ValueError(
                    f"{recipe_id}.{operand_id}: explicit value differs from fact {fact_refs[0]}"
                )
    return value


def _evaluate_recipe(
    recipe: dict[str, Any],
    *,
    facts: dict[str, dict[str, Any]],
    claims: dict[str, dict[str, Any]],
) -> dict[str, Any]:
    recipe_id = str(recipe.get("recipe_id", "")).strip()
    if not recipe_id:
        return {
            "recipe_id": "",
            "status": "calculation_not_run",
            "errors": ["recipe_id is required"],
        }
    errors: list[str] = []
    if recipe.get("review_status") != "confirmed":
        errors.append("recipe review_status must be confirmed")
    approval = recipe.get("approval")
    if not isinstance(approval, dict):
        errors.append("professional recipe approval is required")
    else:
        if not str(approval.get("approved_by_id", "")).strip():
            errors.append("approval.approved_by_id is required")
        if approval.get("approved_by_role") != "professional_reviewer":
            errors.append("approval must come from a professional_reviewer")
        if not _valid_approval_datetime(approval.get("recorded_at")):
            errors.append("approval.recorded_at must include an ISO timezone")
        if not str(approval.get("basis", "")).strip():
            errors.append("approval.basis is required")
    basis_claim_id = str(recipe.get("formula_basis_claim_id", "")).strip()
    if not basis_claim_id:
        errors.append("formula_basis_claim_id is required")
    elif basis_claim_id not in claims:
        errors.append(f"unknown formula basis claim: {basis_claim_id}")
    elif claims[basis_claim_id].get("verdict") != "supported":
        errors.append(f"formula basis claim is not fully supported: {basis_claim_id}")
    elif claims[basis_claim_id].get("claim_type") != "calculation_basis":
        errors.append(
            f"formula basis claim must have claim_type calculation_basis: {basis_claim_id}"
        )

    rounding = recipe.get("rounding")
    if not isinstance(rounding, dict):
        errors.append("rounding object is required")
        rounding = {}
    places = rounding.get("places")
    mode_name = str(rounding.get("mode", ""))
    if not isinstance(places, int) or not 0 <= places <= 8:
        errors.append("rounding.places must be an integer from 0 to 8")
    if mode_name not in ROUNDING_MODES:
        errors.append(f"unsupported rounding mode: {mode_name or '<empty>'}")

    values: dict[str, Decimal] = {}
    operands = recipe.get("operands")
    if not isinstance(operands, list) or not operands:
        errors.append("at least one operand is required")
        operands = []
    for operand in operands:
        if not isinstance(operand, dict):
            errors.append("operand must be an object")
            continue
        operand_id = str(operand.get("id", "")).strip()
        if operand_id in values:
            errors.append(f"duplicate operand id: {operand_id}")
            continue
        try:
            values[operand_id] = _validate_operand(
                operand,
                fact_map=facts,
                claim_map=claims,
                recipe_id=recipe_id,
            )
        except ValueError as exc:
            errors.append(str(exc))

    step_results: list[dict[str, str]] = []
    steps = recipe.get("steps")
    if not isinstance(steps, list) or not steps:
        errors.append("at least one calculation step is required")
        steps = []
    for step in steps:
        if not isinstance(step, dict):
            errors.append("calculation step must be an object")
            continue
        step_id = str(step.get("id", "")).strip()
        operation = str(step.get("operation", ""))
        inputs = step.get("inputs")
        if not step_id or step_id in values:
            errors.append(f"invalid or duplicate step id: {step_id or '<empty>'}")
            continue
        if operation not in OPERATIONS:
            errors.append(f"{step_id}: unsupported operation {operation or '<empty>'}")
            continue
        if not isinstance(inputs, list) or len(inputs) != 2:
            errors.append(f"{step_id}: exactly two inputs are required")
            continue
        left_id, right_id = map(str, inputs)
        if left_id not in values or right_id not in values:
            errors.append(
                f"{step_id}: input ids must refer to earlier operands or steps"
            )
            continue
        try:
            result = _calculate(operation, values[left_id], values[right_id])
        except (DivisionByZero, InvalidOperation, ZeroDivisionError):
            errors.append(f"{step_id}: invalid decimal operation")
            continue
        values[step_id] = result
        step_results.append(
            {
                "step_id": step_id,
                "operation": operation,
                "left": str(values[left_id]),
                "right": str(values[right_id]),
                "raw_result": str(result),
            }
        )

    if (
        errors
        or not step_results
        or not isinstance(places, int)
        or mode_name not in ROUNDING_MODES
    ):
        return {
            "recipe_id": recipe_id,
            "description": recipe.get("description", ""),
            "status": "calculation_not_run",
            "errors": errors or ["no valid calculation result"],
            "steps": step_results,
        }
    final_raw = values[step_results[-1]["step_id"]]
    quantum = Decimal(1).scaleb(-places)
    final_result = final_raw.quantize(quantum, rounding=ROUNDING_MODES[mode_name])
    return {
        "recipe_id": recipe_id,
        "description": recipe.get("description", ""),
        "period": recipe.get("period", {}),
        "formula_basis_claim_id": basis_claim_id,
        "status": "calculated",
        "steps": step_results,
        "raw_result": str(final_raw),
        "result": str(final_result),
        "unit": recipe.get("result_unit", "EUR"),
        "rounding": {"places": places, "mode": mode_name},
        "errors": [],
    }


def evaluate_recipes(
    recipes_payload: dict[str, Any],
    records_payload: dict[str, Any],
    claims_payload: dict[str, Any],
) -> dict[str, Any]:
    """Evaluate only explicitly confirmed arithmetic recipes.

    Determinism is justified here because the operations are exact arithmetic with
    an explicit formula, operands, provenance, and rounding contract. This function
    never chooses a contribution rate, legal regime, threshold, ceiling, or formula.
    """

    recipes = recipes_payload.get("recipes")
    if not isinstance(recipes, list):
        raise ValueError("recipes must be a list")
    facts = _fact_map(records_payload)
    claims = _claim_map(claims_payload)
    results = [
        _evaluate_recipe(recipe, facts=facts, claims=claims)
        for recipe in recipes
        if isinstance(recipe, dict)
    ]
    not_run = sum(result["status"] != "calculated" for result in results)
    return {
        "schema_version": "1.0",
        "plugin": "previdenza-inps",
        "evaluated_at": _utc_now(),
        "status": "passed" if results and not not_run else "calculation_not_run",
        "recipe_count": len(results),
        "calculated_count": len(results) - not_run,
        "not_run_count": not_run,
        "results": results,
        "semantic_scope": "not_performed",
    }


def _write_results_csv(path: Path, result: dict[str, Any]) -> None:
    fieldnames = [
        "recipe_id",
        "description",
        "status",
        "result",
        "unit",
        "formula_basis_claim_id",
        "errors",
    ]
    with path.open("w", encoding="utf-8", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=fieldnames)
        writer.writeheader()
        for row in result["results"]:
            writer.writerow(
                {
                    "recipe_id": row.get("recipe_id", ""),
                    "description": row.get("description", ""),
                    "status": row.get("status", ""),
                    "result": row.get("result", ""),
                    "unit": row.get("unit", ""),
                    "formula_basis_claim_id": row.get("formula_basis_claim_id", ""),
                    "errors": "; ".join(row.get("errors", [])),
                }
            )
    mark_private_file(path)


def main(argv: list[str] | None = None) -> int:
    """Run reviewer-approved calculations and write audit artifacts."""

    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("recipes", type=Path)
    parser.add_argument("case_records", type=Path)
    parser.add_argument("claims_review", type=Path)
    parser.add_argument("--output-dir", type=Path, required=True)
    parser.add_argument("--client-engagement", type=Path, required=True)
    args = parser.parse_args(argv)
    try:
        context = load_client_engagement_context_file(
            args.client_engagement,
            expected_workflow_id="previdenza-inps",
            input_paths=[args.recipes, args.case_records, args.claims_review],
            output_dir=args.output_dir,
        )
        output_dir = ensure_safe_output_dir(args.output_dir, plugin_root=PLUGIN_ROOT)
        run_root = Path(context["run_root"])
        result = evaluate_recipes(
            _load_object(args.recipes),
            _load_object(args.case_records),
            _load_object(args.claims_review),
        )
        result["input_provenance"] = {
            "recipes": {
                "path": _run_relative_path(args.recipes, run_root),
                "sha256": _file_sha256(args.recipes),
            },
            "case_records": {
                "path": _run_relative_path(args.case_records, run_root),
                "sha256": _file_sha256(args.case_records),
            },
            "claims_review": {
                "path": _run_relative_path(args.claims_review, run_root),
                "sha256": _file_sha256(args.claims_review),
            },
        }
        prepare_private_directory(output_dir)
        results_path = write_json(output_dir / "calculation_results.json", result)
        csv_path = output_dir / "calculation_results.csv"
        _write_results_csv(csv_path, result)
        write_json(
            output_dir / "calculation_audit.json",
            {
                "schema_version": "1.0",
                "status": result["status"],
                "path_reference": "run_root_relative",
                "calculation_results_path": _run_relative_path(results_path, run_root),
                "calculation_results_sha256": _file_sha256(results_path),
                "calculation_results_csv_path": _run_relative_path(csv_path, run_root),
                "calculation_results_csv_sha256": _file_sha256(csv_path),
                "input_provenance": result["input_provenance"],
                "recipe_count": result["recipe_count"],
                "calculated_count": result["calculated_count"],
                "not_run_count": result["not_run_count"],
                "issues": [
                    {"recipe_id": row["recipe_id"], "errors": row["errors"]}
                    for row in result["results"]
                    if row["errors"]
                ],
            },
        )
    except (
        AssuranceContractError,
        FileNotFoundError,
        json.JSONDecodeError,
        PermissionError,
        ValueError,
    ) as exc:
        LOGGER.error("%s", exc)
        return 1
    if result["status"] != "passed":
        LOGGER.error("One or more calculations were not run.")
        return 1
    LOGGER.info("Calculated %s approved recipe(s).", result["calculated_count"])
    return 0


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO, format="%(message)s")
    raise SystemExit(main())

SHA-256: d65dcc717f36788ac14ee590188c7a07cc4d9b226dc7a72876f3578772931276