diff --git a/src/policyengine_api/api/analysis.py b/src/policyengine_api/api/analysis.py index 20882e9..f65345a 100644 --- a/src/policyengine_api/api/analysis.py +++ b/src/policyengine_api/api/analysis.py @@ -6,6 +6,13 @@ This is an async operation - the endpoint returns immediately with a report_id, and you poll /analysis/economic-impact/{report_id} until status is "completed". + +WORKFLOW for full economic analysis: +1. Create a policy with parameter changes: POST /policies +2. Get a dataset: GET /datasets (look for UK/US datasets) +3. Start analysis: POST /analysis/economic-impact with policy_id and dataset_id +4. Check status: GET /analysis/economic-impact/{report_id} - repeat until status="completed" +5. Review results: The completed response includes decile_impacts and program_statistics """ import math diff --git a/src/policyengine_api/api/datasets.py b/src/policyengine_api/api/datasets.py index fa68cb8..12481f0 100644 --- a/src/policyengine_api/api/datasets.py +++ b/src/policyengine_api/api/datasets.py @@ -23,6 +23,10 @@ def list_datasets(session: Session = Depends(get_session)): Returns datasets that can be used with the /analysis/economic-impact endpoint. Each dataset represents population microdata for a specific country and year. + + USAGE: For UK analysis, look for datasets with names containing "uk" or "frs". + For US analysis, look for datasets with names containing "us" or "cps". + Use the dataset's id when calling /analysis/economic-impact. """ datasets = session.exec(select(Dataset)).all() return datasets diff --git a/src/policyengine_api/api/parameters.py b/src/policyengine_api/api/parameters.py index a3d7b76..55c90f4 100644 --- a/src/policyengine_api/api/parameters.py +++ b/src/policyengine_api/api/parameters.py @@ -21,15 +21,31 @@ @router.get("/", response_model=List[ParameterRead]) @cache(expire=3600) # Cache for 1 hour def list_parameters( - skip: int = 0, limit: int = 100, session: Session = Depends(get_session) + skip: int = 0, + limit: int = 100, + search: str | None = None, + session: Session = Depends(get_session), ): - """List available parameters with pagination. + """List available parameters with pagination and search. Parameters are policy levers (e.g. tax rates, thresholds, benefit amounts) that can be modified in reforms. Use parameter names when creating policies. + + Use the `search` parameter to filter by parameter name, label, or description. + For example: search="basic_rate" or search="income tax" """ + query = select(Parameter) + + if search: + search_filter = ( + Parameter.name.contains(search) + | Parameter.label.contains(search) + | Parameter.description.contains(search) + ) + query = query.where(search_filter) + parameters = session.exec( - select(Parameter).order_by(Parameter.name).offset(skip).limit(limit) + query.order_by(Parameter.name).offset(skip).limit(limit) ).all() return parameters diff --git a/src/policyengine_api/api/policies.py b/src/policyengine_api/api/policies.py index 00a38c9..d0e2ca5 100644 --- a/src/policyengine_api/api/policies.py +++ b/src/policyengine_api/api/policies.py @@ -3,15 +3,44 @@ Policies represent tax-benefit parameter reforms that can be compared against baseline (current law). Create a policy, then use its ID with the household calculation or economic impact endpoints to see the reform's effects. + +WORKFLOW: To analyze a policy reform (e.g. lowering UK basic income tax rate to 16%): + +1. Search for the parameter: GET /parameters?search=basic_rate +2. Note the parameter_id from the results +3. Create a policy with parameter values: + POST /policies + { + "name": "Lower basic rate to 16p", + "description": "Reduce UK basic income tax rate from 20p to 16p", + "parameter_values": [ + { + "parameter_id": "", + "value_json": 0.16, + "start_date": "2026-01-01T00:00:00Z", + "end_date": null + } + ] + } +4. Test on a household: POST /household/impact with the policy_id +5. Run population analysis: POST /analysis/economic-impact with policy_id and dataset_id +6. Poll GET /analysis/economic-impact/{report_id} until status="completed" """ +from datetime import datetime from typing import List from uuid import UUID from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select -from policyengine_api.models import Policy, PolicyCreate, PolicyRead +from policyengine_api.models import ( + Parameter, + ParameterValue, + Policy, + PolicyCreate, + PolicyRead, +) from policyengine_api.services.database import get_session router = APIRouter(prefix="/policies", tags=["policies"]) @@ -19,13 +48,64 @@ @router.post("/", response_model=PolicyRead) def create_policy(policy: PolicyCreate, session: Session = Depends(get_session)): - """Create a new policy reform. + """Create a new policy reform with parameter values. Policies define changes to tax-benefit parameters. After creating a policy, use its ID with /household/calculate or /analysis/economic-impact to see effects. + + Include parameter_values in the request to specify which parameters to change: + { + "name": "Lower basic rate to 16p", + "description": "Reduce UK basic income tax rate from 20p to 16p", + "parameter_values": [ + { + "parameter_id": "uuid-from-parameters-search", + "value_json": 0.16, + "start_date": "2026-01-01T00:00:00Z", + "end_date": null + } + ] + } """ - db_policy = Policy.model_validate(policy) + # Create the policy + db_policy = Policy(name=policy.name, description=policy.description) session.add(db_policy) + session.flush() # Get the policy ID before adding parameter values + + # Create associated parameter values + for pv_data in policy.parameter_values: + # Validate parameter exists + param = session.get(Parameter, pv_data["parameter_id"]) + if not param: + raise HTTPException( + status_code=404, + detail=f"Parameter {pv_data['parameter_id']} not found", + ) + + # Parse dates + start_date = ( + datetime.fromisoformat(pv_data["start_date"].replace("Z", "+00:00")) + if isinstance(pv_data["start_date"], str) + else pv_data["start_date"] + ) + end_date = None + if pv_data.get("end_date"): + end_date = ( + datetime.fromisoformat(pv_data["end_date"].replace("Z", "+00:00")) + if isinstance(pv_data["end_date"], str) + else pv_data["end_date"] + ) + + # Create parameter value + db_pv = ParameterValue( + parameter_id=pv_data["parameter_id"], + value_json=pv_data["value_json"], + start_date=start_date, + end_date=end_date, + policy_id=db_policy.id, + ) + session.add(db_pv) + session.commit() session.refresh(db_policy) return db_policy diff --git a/src/policyengine_api/models/policy.py b/src/policyengine_api/models/policy.py index 202e33f..570320b 100644 --- a/src/policyengine_api/models/policy.py +++ b/src/policyengine_api/models/policy.py @@ -29,9 +29,29 @@ class Policy(PolicyBase, table=True): class PolicyCreate(PolicyBase): - """Schema for creating policies.""" - - pass + """Schema for creating policies. + + When creating a policy with parameter values, provide a list of + parameter value definitions. Each parameter value needs: + - parameter_id: UUID of the parameter to modify + - value_json: The new value (number, string, or nested object) + - start_date: When this value takes effect + - end_date: Optional end date (null for indefinite) + + Example: + { + "name": "Lower basic rate to 16p", + "description": "Reduce UK basic income tax rate", + "parameter_values": [{ + "parameter_id": "uuid-here", + "value_json": 0.16, + "start_date": "2026-01-01T00:00:00Z", + "end_date": null + }] + } + """ + + parameter_values: list[dict] = [] class PolicyRead(PolicyBase):