Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 32 additions & 8 deletions crossroads/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from flask import Flask, jsonify, request
from flask_cors import CORS

from .compare import calculate_cliff_analysis, compare
from .compare import calculate_cliff_analysis, compare, compare_multiple
from .events import (
ChildAgingOut,
Divorce,
Expand Down Expand Up @@ -157,16 +157,40 @@ def format_result_for_frontend(result) -> dict:

@app.route("/api/simulate", methods=["POST"])
def simulate():
"""Run a life event simulation."""
"""Run a life event simulation (single or multiple events)."""
try:
data = request.get_json()
household = create_household_from_request(data.get("household", {}))
event = create_event_from_request(
data.get("lifeEvent", {}).get("type"),
data.get("lifeEvent", {}).get("params", {}),
household,
)
result = compare(household, event)

# Support both single event and multiple events
life_events = data.get("lifeEvents", [])
single_event = data.get("lifeEvent")

if life_events:
# Multiple events mode
events = []
current_household = household
for evt in life_events:
event = create_event_from_request(
evt.get("type"),
evt.get("params", {}),
current_household,
)
events.append(event)
# Update household for next event's context
current_household = event.apply(current_household)
result = compare_multiple(household, events)
elif single_event:
# Single event mode (backwards compatible)
event = create_event_from_request(
single_event.get("type"),
single_event.get("params", {}),
household,
)
result = compare(household, event)
else:
raise ValueError("No life event provided")

return jsonify(format_result_for_frontend(result))
except ValueError as e:
return jsonify({"error": str(e)}), 400
Expand Down
68 changes: 68 additions & 0 deletions crossroads/compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,74 @@ def compare(
)


@dataclass
class CombinedEvent(LifeEvent):
"""A wrapper that combines multiple life events into one."""

events: list[LifeEvent] = field(default_factory=list)

@property
def name(self) -> str:
if not self.events:
return "No events"
return " + ".join(e.name for e in self.events)

@property
def description(self) -> str:
if not self.events:
return "No life events selected"
return ", ".join(e.description for e in self.events)

def apply(self, household: Household) -> Household:
"""Apply all events in sequence."""
current = household
for event in self.events:
current = event.apply(current)
return current

def validate(self, household: Household) -> list[str]:
"""Validate all events can be applied in sequence."""
errors = []
current = household
for i, event in enumerate(self.events):
event_errors = event.validate(current)
if event_errors:
errors.extend([f"Event {i+1} ({event.name}): {e}" for e in event_errors])
else:
# Apply event to validate next one against updated household
current = event.apply(current)
return errors


def compare_multiple(
household: Household,
events: list[LifeEvent],
variables: list[str] | None = None,
) -> ComparisonResult:
"""
Compare a household before and after multiple life events.

Events are applied in sequence, and the result shows the baseline
household compared to the final state after all events.

Args:
household: The baseline household before events.
events: List of life events to apply in order.
variables: Optional list of variables to compare.

Returns:
A ComparisonResult with the combined event.
"""
if not events:
raise ValueError("At least one life event is required")

if len(events) == 1:
return compare(household, events[0], variables)

combined = CombinedEvent(events=events)
return compare(household, combined, variables)


def calculate_cliff_analysis(
household: Household,
income_min: int = 0,
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading