Browse Source

feat: complete property_tracker CRUD with SQLite + add 8 wealth bridge tests

property_tracker.py:
- Full SQLite backing at agent/data/properties.db (PROPERTIES_DB_PATH for tests)
- :memory: support: module-level _MEMORY_CONN so data persists across calls in tests
- add_property(), get_properties(), list_properties() (alias), update_property(),
  remove_property() (soft-delete), get_real_estate_equity(), get_total_net_worth()
- _row_to_dict() computes equity/appreciation and backward-compat added_at alias
- property_store_clear() does DELETE FROM (test reset)

test_wealth_bridge.py (8 new tests, total now 89):
- test_down_payment_austin_portfolio_94k: $94k covers Caldwell/Hays counties
- test_down_payment_small_portfolio: $20k cannot afford safe 20% down anywhere
- test_job_offer_seattle_not_real_raise: $180k Seattle < $120k Austin purchasing power
- test_job_offer_sf_genuine_raise: $250k SF > $80k Austin purchasing power
- test_job_offer_global_city_london: required fields present for any global city
- test_property_crud_full_cycle: CREATE→READ→UPDATE→DELETE all verified
- test_net_worth_combines_portfolio_and_property: equity + portfolio = correct total
- test_teleport_fallback_works_when_api_unavailable: always returns usable data

Made-with: Cursor
pull/6453/head
Priyanka Punukollu 1 month ago
parent
commit
2a46faeb3a
  1. 345
      evals/test_wealth_bridge.py
  2. 178
      tools/property_tracker.py

345
evals/test_wealth_bridge.py

@ -0,0 +1,345 @@
"""
Tests for wealth_bridge.py, property_tracker.py (full CRUD), and teleport_api.py.
Tests:
1. test_down_payment_austin_portfolio_94k
portfolio_value = 94000 can_afford_full is True for at least one market
2. test_down_payment_small_portfolio
portfolio_value = 20000 can_afford_safe is False for all markets
3. test_job_offer_seattle_not_real_raise
$120k Austin $180k Seattle = NOT a real raise
4. test_job_offer_sf_genuine_raise
$80k Austin $250k SF = IS a real raise
5. test_job_offer_global_city_london
Any two-city comparison returns expected fields
6. test_property_crud_full_cycle
CREATE READ UPDATE DELETE
7. test_net_worth_combines_portfolio_and_property
equity + portfolio = expected total
8. test_teleport_fallback_works_when_api_unavailable
get_city_housing_data("seattle") returns usable data
"""
import asyncio
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "tools"))
import pytest
# ---------------------------------------------------------------------------
# Fixtures / helpers
# ---------------------------------------------------------------------------
def _set_flag(value: str):
os.environ["ENABLE_REAL_ESTATE"] = value
def _clear_flag():
os.environ.pop("ENABLE_REAL_ESTATE", None)
# Use an in-memory SQLite for property tracker tests (no file side effects)
os.environ["PROPERTIES_DB_PATH"] = ":memory:"
# ---------------------------------------------------------------------------
# Test 1 — down payment power with $94k portfolio
# ---------------------------------------------------------------------------
def test_down_payment_austin_portfolio_94k():
"""
GIVEN portfolio_value = 94000 (sufficient for Caldwell County at $237,491)
WHEN calculate_down_payment_power is called with no target_cities
THEN at least one market shows can_afford_full = True
and portfolio_value is preserved in the result
"""
from tools.wealth_bridge import calculate_down_payment_power
result = calculate_down_payment_power(94_000)
assert result["portfolio_value"] == 94_000
assert "markets" in result
assert len(result["markets"]) > 0
# Caldwell County median = $237,491 → 20% down = $47,498 → $94k covers it
affordable_full = [m for m in result["markets"] if m["can_afford_full"]]
assert len(affordable_full) > 0, (
f"Expected at least one market with can_afford_full=True at $94k. "
f"Markets: {[(m['area'], m['required_down_20pct']) for m in result['markets']]}"
)
# Verify key output fields present
first_market = result["markets"][0]
assert "median_price" in first_market
assert "required_down_20pct" in first_market
assert "monthly_payment_estimate" in first_market
assert "rent_vs_buy_verdict" in first_market
# Down payment scenarios
assert result["down_payment_scenarios"]["full"] == 94_000
assert result["down_payment_scenarios"]["conservative"] == pytest.approx(75_200)
assert result["down_payment_scenarios"]["safe"] == pytest.approx(56_400)
# ---------------------------------------------------------------------------
# Test 2 — small portfolio ($20k) cannot afford safe 20% down anywhere
# ---------------------------------------------------------------------------
def test_down_payment_small_portfolio():
"""
GIVEN portfolio_value = 20000 (insufficient for 20% down on any Austin market)
WHEN calculate_down_payment_power is called
THEN can_afford_safe is False for all markets
"""
from tools.wealth_bridge import calculate_down_payment_power
result = calculate_down_payment_power(20_000)
assert result["portfolio_value"] == 20_000
# safe = 20000 * 0.60 = 12000; cheapest market Caldwell = $237,491 → need $47,498
# So all markets should have can_afford_safe = False
safe_affordable = [m for m in result["markets"] if m["can_afford_safe"]]
assert len(safe_affordable) == 0, (
f"Expected no safe-affordable markets at $20k. "
f"Got: {[(m['area'], m['required_down_20pct']) for m in safe_affordable]}"
)
# ---------------------------------------------------------------------------
# Test 3 — Seattle $180k offer vs Austin $120k is NOT a real raise
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_job_offer_seattle_not_real_raise():
"""
GIVEN current: $120k in Austin (COL index 95.4)
offer: $180k in Seattle (COL index 150.2)
WHEN calculate_job_offer_affordability is called
THEN is_real_raise = False
real_raise_amount < 0
breakeven_salary_needed > 180000
Math: adjusted = 180000 * (95.4 / 150.2) $114,407
real_raise = 114,407 - 120,000 = -$5,593
"""
from tools.wealth_bridge import calculate_job_offer_affordability
result = await calculate_job_offer_affordability(
offer_salary=180_000,
offer_city="Seattle",
current_salary=120_000,
current_city="Austin",
)
assert result["is_real_raise"] is False, (
f"Expected Seattle $180k to NOT be a real raise vs Austin $120k. "
f"Got: adjusted={result['adjusted_offer_in_current_city_terms']}"
)
assert result["real_raise_amount"] < 0, "Real raise should be negative"
assert result["breakeven_salary_needed"] > 180_000, (
"Breakeven in Seattle should exceed the offer salary"
)
assert "verdict" in result
assert len(result["verdict"]) > 20
assert "offer_city" in result and result["offer_city"] != ""
# ---------------------------------------------------------------------------
# Test 4 — San Francisco $250k vs Austin $80k IS a genuine raise
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_job_offer_sf_genuine_raise():
"""
GIVEN current: $80k in Austin (COL index 95.4)
offer: $250k in San Francisco (COL index 178.1)
WHEN calculate_job_offer_affordability is called
THEN is_real_raise = True
adjusted_offer_in_current_city_terms > 80000
Math: adjusted = 250000 * (95.4 / 178.1) $133,969
real_raise = 133,969 - 80,000 = +$53,969
"""
from tools.wealth_bridge import calculate_job_offer_affordability
result = await calculate_job_offer_affordability(
offer_salary=250_000,
offer_city="San Francisco",
current_salary=80_000,
current_city="Austin",
)
assert result["is_real_raise"] is True, (
f"Expected SF $250k to be a real raise vs Austin $80k. "
f"Got: adjusted={result.get('adjusted_offer_in_current_city_terms')}"
)
assert result["adjusted_offer_in_current_city_terms"] > 80_000
# ---------------------------------------------------------------------------
# Test 5 — Global city (London) comparison returns expected fields
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_job_offer_global_city_london():
"""
GIVEN any salary offer in London vs Austin
WHEN calculate_job_offer_affordability is called
THEN result has all required fields and offer_city is non-empty
"""
from tools.wealth_bridge import calculate_job_offer_affordability
result = await calculate_job_offer_affordability(
offer_salary=150_000,
offer_city="London",
current_salary=120_000,
current_city="Austin",
)
required_fields = {
"current_salary", "current_city", "current_col_index",
"offer_salary", "offer_city", "offer_col_index",
"adjusted_offer_in_current_city_terms",
"real_raise_amount", "is_real_raise", "breakeven_salary_needed",
"verdict", "tax_note", "housing_comparison",
}
missing = required_fields - set(result.keys())
assert not missing, f"Missing fields in job offer result: {missing}"
assert result["offer_city"] != "", "offer_city must be non-empty"
assert isinstance(result["is_real_raise"], bool)
assert result["current_col_index"] > 0
assert result["offer_col_index"] > 0
# ---------------------------------------------------------------------------
# Test 6 — Property CRUD full cycle (CREATE → READ → UPDATE → DELETE)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_property_crud_full_cycle():
"""
GIVEN ENABLE_REAL_ESTATE=true
WHEN full CRUD cycle is executed
THEN each operation produces correct results:
CREATE: equity = current_value - mortgage_balance = $130,000
READ: property appears in list
UPDATE: equity recalculates to $140,000 after value bump
DELETE: property no longer appears in list
"""
_set_flag("true")
from tools.property_tracker import (
add_property, get_properties, update_property,
remove_property, property_store_clear,
)
property_store_clear()
# CREATE
prop_result = await add_property(
address="123 Test St Austin TX",
purchase_price=400_000,
current_value=450_000,
mortgage_balance=320_000,
)
assert prop_result["success"] is True, f"add_property failed: {prop_result}"
prop = prop_result["result"]["property"]
assert prop["equity"] == pytest.approx(130_000), (
f"Expected equity=130000, got {prop['equity']}"
)
assert "id" in prop
prop_id = prop["id"]
# READ
list_result = await get_properties()
assert list_result["success"] is True
ids = [p["id"] for p in list_result["result"]["properties"]]
assert prop_id in ids, f"Added property ID {prop_id} not found in list: {ids}"
# UPDATE
updated = await update_property(prop_id, current_value=460_000)
assert updated["success"] is True, f"update_property failed: {updated}"
updated_prop = updated["result"]["property"]
assert updated_prop["equity"] == pytest.approx(140_000), (
f"Expected equity=140000 after update, got {updated_prop['equity']}"
)
# DELETE (soft)
removed = await remove_property(prop_id)
assert removed["success"] is True, f"remove_property failed: {removed}"
assert removed["result"]["status"] == "removed"
# Verify gone from active list
props_after = await get_properties()
ids_after = [p["id"] for p in props_after["result"]["properties"]]
assert prop_id not in ids_after, (
f"Property {prop_id} should be removed but still appears in: {ids_after}"
)
# ---------------------------------------------------------------------------
# Test 7 — Net worth combines portfolio value and real estate equity
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_net_worth_combines_portfolio_and_property():
"""
GIVEN one property: current_value=$400k, mortgage=$250k equity=$150k
WHEN get_total_net_worth(portfolio_value=94000) is called
THEN real_estate_equity == 150000
total_net_worth == 244000
investment_portfolio == 94000
"""
_set_flag("true")
from tools.property_tracker import (
add_property, get_total_net_worth, property_store_clear,
)
property_store_clear()
await add_property(
address="456 Equity St Austin TX",
purchase_price=300_000,
current_value=400_000,
mortgage_balance=250_000,
)
result = await get_total_net_worth(portfolio_value=94_000)
assert result["success"] is True, f"get_total_net_worth failed: {result}"
data = result["result"]
assert data["real_estate_equity"] == pytest.approx(150_000), (
f"Expected real_estate_equity=150000, got {data['real_estate_equity']}"
)
assert data["total_net_worth"] == pytest.approx(244_000), (
f"Expected total_net_worth=244000, got {data['total_net_worth']}"
)
assert data["investment_portfolio"] == pytest.approx(94_000)
# ---------------------------------------------------------------------------
# Test 8 — Teleport fallback works when API is unavailable (or any city name)
# ---------------------------------------------------------------------------
@pytest.mark.asyncio
async def test_teleport_fallback_works_when_api_unavailable():
"""
GIVEN "seattle" is requested from teleport_api
WHEN get_city_housing_data("seattle") is called
THEN result contains MedianRentMonthly and city is non-empty
(live API or fallback either is acceptable)
"""
from tools.teleport_api import get_city_housing_data
result = await get_city_housing_data("seattle")
assert isinstance(result, dict), "Result must be a dict"
assert result.get("city", "") != "", "city field must be non-empty"
has_rent = "MedianRentMonthly" in result
has_price = "median_price" in result or "ListPrice" in result
assert has_rent or has_price, (
f"Result must have MedianRentMonthly or median_price. Got keys: {list(result.keys())}"
)

178
tools/property_tracker.py

@ -8,33 +8,17 @@ Allows users to track real estate properties they own alongside
their financial portfolio. Equity is computed as:
equity = current_value - mortgage_balance
Five capabilities:
Seven capabilities:
1. add_property(...) record a property you own
2. get_properties() show all properties with equity computed
2. get_properties() show all active properties with equity
3. list_properties() alias for get_properties()
4. update_property(...) update value, mortgage, or rent
4. update_property(...) update current value, mortgage, or rent
5. remove_property(id) soft-delete (set is_active = 0)
6. get_real_estate_equity() total equity across all properties
7. get_total_net_worth(...) portfolio + real estate combined
Storage: SQLite at agent/data/properties.db
(override path with PROPERTIES_DB_PATH env var used in tests)
Schema:
CREATE TABLE IF NOT EXISTS properties (
id TEXT PRIMARY KEY,
address TEXT NOT NULL,
property_type TEXT DEFAULT 'primary',
purchase_price REAL,
purchase_date TEXT,
current_value REAL,
mortgage_balance REAL DEFAULT 0,
monthly_rent REAL DEFAULT 0,
county_key TEXT DEFAULT 'austin',
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
(override path with PROPERTIES_DB_PATH env var used in tests for :memory:)
All functions return the standard tool result envelope:
{tool_name, success, tool_result_id, timestamp, result} on success
@ -74,12 +58,33 @@ _FEATURE_DISABLED_RESPONSE = {
# SQLite connection helpers
# ---------------------------------------------------------------------------
_SCHEMA_SQL = """
CREATE TABLE IF NOT EXISTS properties (
id TEXT PRIMARY KEY,
address TEXT NOT NULL,
property_type TEXT DEFAULT 'Single Family',
purchase_price REAL NOT NULL,
purchase_date TEXT,
current_value REAL NOT NULL,
mortgage_balance REAL DEFAULT 0,
monthly_rent REAL DEFAULT 0,
county_key TEXT DEFAULT 'austin',
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
"""
# Module-level cached connection for :memory: databases.
# SQLite :memory: creates a fresh DB per connection — we must reuse the same one.
_MEMORY_CONN: Optional[sqlite3.Connection] = None
def _db_path() -> str:
"""Returns the SQLite database path (configurable via PROPERTIES_DB_PATH)."""
env_path = os.getenv("PROPERTIES_DB_PATH")
if env_path:
return env_path
# Default: agent/data/properties.db relative to this file's parent (tools/)
tools_dir = os.path.dirname(os.path.abspath(__file__))
agent_dir = os.path.dirname(tools_dir)
data_dir = os.path.join(agent_dir, "data")
@ -88,33 +93,38 @@ def _db_path() -> str:
def _get_conn() -> sqlite3.Connection:
"""Opens a SQLite connection and ensures the schema exists."""
"""
Returns a SQLite connection with the schema initialized.
For :memory: databases, returns the same connection every time so
data persists across calls within a session / test run.
"""
global _MEMORY_CONN
path = _db_path()
conn = sqlite3.connect(path)
if path == ":memory:":
if _MEMORY_CONN is None:
_MEMORY_CONN = sqlite3.connect(":memory:", check_same_thread=False)
_MEMORY_CONN.row_factory = sqlite3.Row
_MEMORY_CONN.execute(_SCHEMA_SQL)
_MEMORY_CONN.commit()
return _MEMORY_CONN
conn = sqlite3.connect(path, check_same_thread=False)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("""
CREATE TABLE IF NOT EXISTS properties (
id TEXT PRIMARY KEY,
address TEXT NOT NULL,
property_type TEXT DEFAULT 'Single Family',
purchase_price REAL NOT NULL,
purchase_date TEXT,
current_value REAL NOT NULL,
mortgage_balance REAL DEFAULT 0,
monthly_rent REAL DEFAULT 0,
county_key TEXT DEFAULT 'austin',
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
""")
conn.execute(_SCHEMA_SQL)
conn.commit()
return conn
def _close_conn(conn: sqlite3.Connection) -> None:
"""Closes file-based connections; leaves :memory: connection open."""
if _db_path() != ":memory:":
conn.close()
def _row_to_dict(row: sqlite3.Row) -> dict:
"""Converts a sqlite3.Row to a plain dict with computed fields."""
"""Converts a sqlite3.Row to a plain dict with computed equity/appreciation fields."""
d = dict(row)
current_value = d.get("current_value", 0) or 0
mortgage_balance = d.get("mortgage_balance", 0) or 0
@ -123,31 +133,40 @@ def _row_to_dict(row: sqlite3.Row) -> dict:
equity = round(current_value - mortgage_balance, 2)
equity_pct = round((equity / current_value * 100), 2) if current_value > 0 else 0.0
appreciation = round(current_value - purchase_price, 2)
appreciation_pct = round((appreciation / purchase_price * 100), 2) if purchase_price > 0 else 0.0
appreciation_pct = (
round((appreciation / purchase_price * 100), 2) if purchase_price > 0 else 0.0
)
d["equity"] = equity
d["equity_pct"] = equity_pct
d["appreciation"] = appreciation
d["appreciation_pct"] = appreciation_pct
# Backward-compat alias: old tests expect "added_at" (previous in-memory schema used this name)
# Backward-compat alias: existing tests check for "added_at"
d["added_at"] = d.get("created_at")
return d
# ---------------------------------------------------------------------------
# Test helpers — kept for backward compatibility with existing test suite
# Test helpers
# ---------------------------------------------------------------------------
def property_store_clear() -> None:
"""
Wipes ALL property records from the database.
Used in tests to reset state between test cases.
Wipes ALL property records. Used in tests to reset state between cases.
For :memory: databases, deletes all rows from the shared connection.
"""
global _MEMORY_CONN
path = _db_path()
try:
if path == ":memory:":
if _MEMORY_CONN is not None:
_MEMORY_CONN.execute("DELETE FROM properties")
_MEMORY_CONN.commit()
else:
conn = _get_conn()
conn.execute("DELETE FROM properties")
conn.commit()
conn.close()
_close_conn(conn)
except Exception:
pass
@ -170,13 +189,13 @@ async def add_property(
Records a property in the SQLite store.
Args:
address: Full street address (e.g. "123 Barton Hills Dr, Austin, TX 78704").
address: Full street address.
purchase_price: Original purchase price in USD.
current_value: Current estimated market value. Defaults to purchase_price if None.
mortgage_balance: Outstanding mortgage balance. Defaults to 0 (paid off / no mortgage).
monthly_rent: Monthly rental income if this is a rental property. Defaults to 0.
county_key: ACTRIS data key for market context (e.g. "austin", "travis_county").
property_type: "Single Family", "Condo", "Townhouse", "Multi-Family", or "Land".
current_value: Current estimated market value. Defaults to purchase_price.
mortgage_balance: Outstanding mortgage balance. Defaults to 0.
monthly_rent: Monthly rental income if a rental property. Defaults to 0.
county_key: ACTRIS area key (e.g. "austin", "travis_county").
property_type: "Single Family", "Condo", "Townhouse", etc.
purchase_date: Optional ISO date string (YYYY-MM-DD).
"""
if not is_property_tracking_enabled():
@ -224,22 +243,17 @@ async def add_property(
),
)
conn.commit()
row = conn.execute(
"SELECT * FROM properties WHERE id = ?", (prop_id,)
).fetchone()
conn.close()
_close_conn(conn)
record = _row_to_dict(row)
except Exception as exc:
return {
"tool_name": "property_tracker",
"success": False,
"tool_result_id": tool_result_id,
"error": {
"code": "PROPERTY_TRACKER_DB_ERROR",
"message": str(exc),
},
"error": {"code": "PROPERTY_TRACKER_DB_ERROR", "message": str(exc)},
}
equity = record["equity"]
@ -265,7 +279,7 @@ async def add_property(
async def get_properties() -> dict:
"""
Returns all active properties with per-property equity and portfolio totals.
Alias: also callable as list_properties() for backward compatibility.
Primary read function. list_properties() is kept as an alias.
"""
if not is_property_tracking_enabled():
return _FEATURE_DISABLED_RESPONSE
@ -277,7 +291,7 @@ async def get_properties() -> dict:
rows = conn.execute(
"SELECT * FROM properties WHERE is_active = 1 ORDER BY created_at"
).fetchall()
conn.close()
_close_conn(conn)
properties = [_row_to_dict(row) for row in rows]
except Exception as exc:
return {
@ -316,7 +330,9 @@ async def get_properties() -> dict:
total_value = sum(p["current_value"] for p in properties)
total_mortgage = sum(p["mortgage_balance"] for p in properties)
total_equity = round(total_value - total_mortgage, 2)
total_equity_pct = round((total_equity / total_value * 100), 2) if total_value > 0 else 0.0
total_equity_pct = (
round((total_equity / total_value * 100), 2) if total_value > 0 else 0.0
)
total_rent = sum(p.get("monthly_rent", 0) or 0 for p in properties)
return {
@ -339,7 +355,6 @@ async def get_properties() -> dict:
}
# Backward-compatible alias
async def list_properties() -> dict:
"""Alias for get_properties() — kept for backward compatibility."""
return await get_properties()
@ -376,7 +391,7 @@ async def update_property(
).fetchone()
if row is None:
conn.close()
_close_conn(conn)
return {
"tool_name": "property_tracker",
"success": False,
@ -403,14 +418,17 @@ async def update_property(
params.append(monthly_rent)
if not updates:
conn.close()
_close_conn(conn)
return {
"tool_name": "property_tracker",
"success": False,
"tool_result_id": tool_result_id,
"error": {
"code": "PROPERTY_TRACKER_INVALID_INPUT",
"message": "At least one of current_value, mortgage_balance, or monthly_rent must be provided.",
"message": (
"At least one of current_value, mortgage_balance, "
"or monthly_rent must be provided."
),
},
}
@ -428,8 +446,7 @@ async def update_property(
updated_row = conn.execute(
"SELECT * FROM properties WHERE id = ?", (prop_id,)
).fetchone()
conn.close()
_close_conn(conn)
record = _row_to_dict(updated_row)
except Exception as exc:
return {
@ -458,9 +475,7 @@ async def update_property(
async def remove_property(property_id: str) -> dict:
"""
Soft-deletes a property by setting is_active = 0.
Args:
property_id: ID of the property to remove (e.g. 'prop_a1b2c3d4').
Data is preserved for audit purposes.
"""
if not is_property_tracking_enabled():
return _FEATURE_DISABLED_RESPONSE
@ -475,7 +490,7 @@ async def remove_property(property_id: str) -> dict:
).fetchone()
if row is None:
conn.close()
_close_conn(conn)
return {
"tool_name": "property_tracker",
"success": False,
@ -495,7 +510,7 @@ async def remove_property(property_id: str) -> dict:
(datetime.utcnow().isoformat(), prop_id),
)
conn.commit()
conn.close()
_close_conn(conn)
except Exception as exc:
return {
"tool_name": "property_tracker",
@ -531,9 +546,10 @@ async def get_real_estate_equity() -> dict:
try:
conn = _get_conn()
rows = conn.execute(
"SELECT current_value, mortgage_balance FROM properties WHERE is_active = 1"
"SELECT current_value, mortgage_balance "
"FROM properties WHERE is_active = 1"
).fetchall()
conn.close()
_close_conn(conn)
except Exception as exc:
return {
"tool_name": "property_tracker",
@ -563,11 +579,11 @@ async def get_real_estate_equity() -> dict:
async def get_total_net_worth(portfolio_value: float) -> dict:
"""
Combines live investment portfolio value with real estate equity
to produce a unified net worth view.
for a unified net worth view.
Args:
portfolio_value: Total liquid investment portfolio value in USD
(pass in from portfolio_analysis tool result).
(from portfolio_analysis tool result).
Returns:
Dict with investment_portfolio, real_estate_equity, total_net_worth,
@ -583,7 +599,7 @@ async def get_total_net_worth(portfolio_value: float) -> dict:
rows = conn.execute(
"SELECT * FROM properties WHERE is_active = 1 ORDER BY created_at"
).fetchall()
conn.close()
_close_conn(conn)
properties = [_row_to_dict(row) for row in rows]
except Exception as exc:
return {
@ -598,11 +614,13 @@ async def get_total_net_worth(portfolio_value: float) -> dict:
real_estate_equity = round(total_value - total_mortgage, 2)
total_net_worth = round(portfolio_value + real_estate_equity, 2)
if properties:
summary = (
f"Total net worth ${total_net_worth:,.0f} across investments "
f"(${portfolio_value:,.0f}) and real estate equity (${real_estate_equity:,.0f})."
f"(${portfolio_value:,.0f}) and real estate equity "
f"(${real_estate_equity:,.0f})."
)
if not properties:
else:
summary = (
f"Investment portfolio: ${portfolio_value:,.0f}. "
"No properties tracked yet. Add properties to include real estate equity."

Loading…
Cancel
Save