Skip to content

Commit 3df1018

Browse files
authored
Merge pull request #592 from DavidIfebueme/feat/save-health-factor-to-db
feat: save health ratio factor to db
2 parents 03f38ed + a3448b9 commit 3df1018

File tree

3 files changed

+163
-4
lines changed

3 files changed

+163
-4
lines changed

apps/data_handler/db/models/liquidable_debt.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class LiquidableDebt(Base):
1313
"""
1414

1515
__tablename__ = "liquidable_debt"
16+
__table_args__ = {'extend_existing': True}
1617

1718
liquidable_debt = Column(DECIMAL, nullable=False)
1819
protocol_name = Column(ChoiceType(LendingProtocolNames, impl=String()), nullable=False)
@@ -27,6 +28,7 @@ class HealthRatioLevel(Base):
2728
"""
2829

2930
__tablename__ = "health_ratio_level"
31+
__table_args__ = {'extend_existing': True}
3032

3133
timestamp = Column(BigInteger, index=True)
3234
user_id = Column(String, index=True)

apps/data_handler/handlers/loan_states/vesu/events.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@
88

99
from sqlalchemy import select
1010
from starknet_py.hash.selector import get_selector_from_name
11+
from data_handler.db.models.liquidable_debt import HealthRatioLevel
1112

12-
from apps.data_handler.db.crud import DBConnector
13-
from apps.data_handler.db.models import VesuPosition
14-
from apps.shared.starknet_client import StarknetClient
13+
from data_handler.db.crud import DBConnector
14+
from data_handler.db.models import VesuPosition
15+
from shared.starknet_client import StarknetClient
1516

1617

1718
class VesuLoanEntity:
@@ -46,8 +47,23 @@ async def _get_token_decimals(self, token_address: int) -> Decimal:
4647
decimals = result[0]
4748
return Decimal(10) ** Decimal(decimals)
4849
return Decimal("Inf")
50+
51+
async def save_health_ratio_level(
52+
self, session, timestamp, user_id, value, protocol_id
53+
):
54+
"""Save a HealthRatioLevel record to the DB."""
55+
record = HealthRatioLevel(
56+
timestamp=timestamp,
57+
user_id=user_id,
58+
value=value,
59+
protocol_id=protocol_id,
60+
)
61+
session.add(record)
62+
await session.commit()
63+
await session.refresh(record)
64+
return record
4965

50-
async def calculate_health_factor(self, user_address: int) -> dict[str, Decimal]:
66+
async def calculate_health_factor(self, user_address: int, session=None) -> dict[str, Decimal]:
5167
"""
5268
Calculate health factors for all positions of a user.
5369
@@ -128,6 +144,15 @@ async def calculate_health_factor(self, user_address: int) -> dict[str, Decimal]
128144

129145
results[hex(pool_id)] = health_factor
130146

147+
if session is not None:
148+
await self.save_health_ratio_level(
149+
session=session,
150+
timestamp=pos.get("block_number", 0),
151+
user_id=str(user_address),
152+
value=health_factor,
153+
protocol_id=pool_id,
154+
)
155+
131156
return results
132157

133158
async def _get_position_data(
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import pytest
2+
from decimal import Decimal
3+
from unittest.mock import AsyncMock, MagicMock, patch
4+
5+
from data_handler.handlers.loan_states.vesu.events import VesuLoanEntity
6+
from data_handler.db.models.liquidable_debt import HealthRatioLevel
7+
8+
9+
@pytest.fixture
10+
def vesu_entity():
11+
"""Create VesuLoanEntity instance for testing"""
12+
with patch("data_handler.handlers.loan_states.vesu.events.StarknetClient"), \
13+
patch("data_handler.handlers.loan_states.vesu.events.DBConnector"):
14+
entity = VesuLoanEntity()
15+
entity.session = AsyncMock()
16+
return entity
17+
18+
19+
@pytest.fixture
20+
def mock_session():
21+
"""Mock database session"""
22+
session = AsyncMock()
23+
session.add = MagicMock()
24+
session.commit = AsyncMock()
25+
session.refresh = AsyncMock()
26+
return session
27+
28+
29+
@pytest.fixture
30+
def mock_vesu_position():
31+
"""Mock VesuPosition object"""
32+
position = MagicMock()
33+
position.pool_id = "456"
34+
position.collateral_asset = "789"
35+
position.debt_asset = "101112"
36+
position.get.return_value = 1000000
37+
38+
39+
class TestVesuLoanEntity:
40+
41+
@pytest.mark.asyncio
42+
async def test_save_health_ratio_level(self, vesu_entity, mock_session):
43+
"""Test saving health ratio level to database"""
44+
result = await vesu_entity.save_health_ratio_level(
45+
session=mock_session,
46+
timestamp=1234567890,
47+
user_id="0x123",
48+
value=Decimal("1.5"),
49+
protocol_id="vesu",
50+
)
51+
52+
mock_session.add.assert_called_once()
53+
mock_session.commit.assert_called_once()
54+
mock_session.refresh.assert_called_once()
55+
56+
added_record = mock_session.add.call_args[0][0]
57+
assert isinstance(added_record, HealthRatioLevel)
58+
assert added_record.timestamp == 1234567890
59+
assert added_record.user_id == "0x123"
60+
assert added_record.value == Decimal("1.5")
61+
assert added_record.protocol_id == "vesu"
62+
63+
@pytest.mark.asyncio
64+
async def test_calculate_health_factor_with_session(
65+
self, vesu_entity, mock_session, mock_vesu_position
66+
):
67+
"""Test calculate_health_factor saves to database when session provided"""
68+
mock_scalars = MagicMock()
69+
mock_scalars.all.return_value = [mock_vesu_position]
70+
mock_result = MagicMock()
71+
mock_result.scalars.return_value = mock_scalars
72+
vesu_entity.session.execute = AsyncMock(return_value=mock_result)
73+
74+
with patch.object(
75+
vesu_entity, "_get_position_data"
76+
) as mock_position, patch.object(
77+
vesu_entity, "_get_collateral_value"
78+
) as mock_collateral, patch.object(
79+
vesu_entity, "_get_asset_config"
80+
) as mock_asset_config, patch.object(
81+
vesu_entity, "_calculate_debt"
82+
) as mock_debt, patch.object(
83+
vesu_entity, "get_ltv_config"
84+
) as mock_ltv, patch.object(
85+
vesu_entity, "_get_token_decimals"
86+
) as mock_decimals, patch.object(
87+
vesu_entity, "fetch_token_price"
88+
) as mock_price, patch.object(
89+
vesu_entity, "save_health_ratio_level"
90+
) as mock_save:
91+
92+
mock_position.return_value = (
93+
100,
94+
0,
95+
200,
96+
0,
97+
)
98+
mock_collateral.return_value = Decimal("1000")
99+
mock_asset_config.return_value = [
100+
0
101+
] * 16
102+
mock_debt.return_value = Decimal("500")
103+
mock_ltv.return_value = (Decimal("80"),)
104+
mock_decimals.return_value = Decimal("1000000")
105+
mock_price.return_value = Decimal("2000")
106+
107+
result = await vesu_entity.calculate_health_factor(
108+
123, session=mock_session
109+
)
110+
111+
mock_save.assert_called_once_with(
112+
session=mock_session,
113+
timestamp=1000000,
114+
user_id="123",
115+
value=result["0x1c8"],
116+
protocol_id=456,
117+
)
118+
119+
@pytest.mark.asyncio
120+
async def test_calculate_health_factor_without_session(self, vesu_entity):
121+
"""Test calculate_health_factor doesn't save when no session provided"""
122+
mock_scalars = MagicMock()
123+
mock_scalars.all.return_value = []
124+
mock_result = MagicMock()
125+
mock_result.scalars.return_value = mock_scalars
126+
vesu_entity.session.execute = AsyncMock(return_value=mock_result)
127+
128+
with patch.object(vesu_entity, "save_health_ratio_level") as mock_save:
129+
result = await vesu_entity.calculate_health_factor(123)
130+
131+
mock_save.assert_not_called()
132+
assert result == {}

0 commit comments

Comments
 (0)