1- from sqlalchemy import Column , Integer , String , Float , Boolean , DateTime , text
2- from sqlalchemy .ext .asyncio import create_async_engine , AsyncSession
3- from sqlalchemy .orm import sessionmaker , declarative_base
41from datetime import datetime
52import pytz
3+ from sqlalchemy import Column , Integer , String , Float , Boolean , DateTime , text
4+ from sqlalchemy .ext .asyncio import create_async_engine , AsyncSession , AsyncEngine
5+ from sqlalchemy .orm import sessionmaker , declarative_base
6+ from sqlalchemy .future import select
7+
68from config import SETTINGS
79
8- # Используем вашу таймзону
9- TIMEZONE = pytz .timezone ('Asia/Makassar' )
10+ # Define Timezone
11+ TIMEZONE = pytz .timezone ('Asia/Makassar' )
1012
1113Base = declarative_base ()
1214
1315class Service (Base ):
16+ """Database model representing a tracked service."""
1417 __tablename__ = 'services'
1518
1619 id = Column (Integer , primary_key = True )
1720 name = Column (String , unique = True , nullable = False )
1821
19- # Мониторинг баланса
22+ # Balance Monitoring
2023 last_balance = Column (Float , default = 0.0 )
2124 low_balance_alert_sent = Column (Boolean , default = False )
2225
23- # Новые поля (согласно твоему скриншоту)
26+ # Financial Configuration
2427 currency = Column (String , default = "USD" ) # USD, RUB, UAH
25- daily_cost = Column (Float , nullable = True ) # Расход в день
26- monthly_fee = Column (Float , nullable = True ) # Ежемесячный платеж
28+ daily_cost = Column (Float , nullable = True ) # Estimated daily cost
29+ monthly_fee = Column (Float , nullable = True ) # Fixed monthly fee
2730
28- # Даты оповещений
31+ # Alert Schedule
2932 next_alert_date = Column (DateTime , nullable = True )
30- next_monthly_alert = Column (DateTime , nullable = True ) # Для ежемесячных подписок
33+ next_monthly_alert = Column (DateTime , nullable = True )
3134
3235 def __repr__ (self ):
3336 return f"<Service(name='{ self .name } ', balance={ self .last_balance } )>"
3437
35- # Инициализация
36- async def init_db ( database_url : str ):
38+ async def init_db ( database_url : str ) -> sessionmaker :
39+ """Initialize the database engine and session factory."""
3740 engine = create_async_engine (database_url , echo = False )
3841 async with engine .begin () as conn :
3942 await conn .run_sync (Base .metadata .create_all )
4043
41- AsyncSessionLocal = sessionmaker (
44+ async_session = sessionmaker (
4245 autocommit = False ,
4346 autoflush = False ,
4447 bind = engine ,
4548 class_ = AsyncSession ,
4649 expire_on_commit = False ,
4750 )
48- return AsyncSessionLocal
51+ return async_session
4952
50- # Вспомогательная функция для добавления начальных данных
51- async def initialize_services (SessionLocal ):
52- async with SessionLocal () as session :
53- # --- simple sqlite migration to add new columns if missing ---
53+ async def initialize_services (session_factory ) -> None :
54+ """
55+ Populate the database with default services and perform schema migrations if necessary.
56+ """
57+ async with session_factory () as session :
58+ # 1. Simple schema migration (add columns if missing)
5459 pragma_stmt = text ("PRAGMA table_info(services)" )
5560 result = await session .execute (pragma_stmt )
56- columns = {row [1 ] for row in result .fetchall ()}
61+ existing_columns = {row [1 ] for row in result .fetchall ()}
5762
5863 alter_statements = []
59- if 'currency' not in columns :
60- alter_statements .append ("ALTER TABLE services ADD COLUMN currency VARCHAR" )
61- if 'daily_cost' not in columns :
62- alter_statements .append ("ALTER TABLE services ADD COLUMN daily_cost FLOAT" )
63- if 'monthly_fee' not in columns :
64- alter_statements .append ("ALTER TABLE services ADD COLUMN monthly_fee FLOAT" )
65- if 'next_monthly_alert' not in columns :
66- alter_statements .append ("ALTER TABLE services ADD COLUMN next_monthly_alert DATETIME" )
64+ required_columns = ['currency' , 'daily_cost' , 'monthly_fee' , 'next_monthly_alert' ]
65+
66+ for col in required_columns :
67+ if col not in existing_columns :
68+ col_type = 'DATETIME' if 'next' in col else ('FLOAT' if 'cost' in col or 'fee' in col else 'VARCHAR' )
69+ alter_statements .append (f"ALTER TABLE services ADD COLUMN { col } { col_type } " )
6770
6871 for stmt in alter_statements :
6972 try :
7073 await session .execute (text (stmt ))
7174 except Exception :
72- # ignore if already exists or other minor issues
73- pass
75+ pass # Ignore errors if column exists
76+
7477 if alter_statements :
7578 await session .commit ()
7679
77- services_to_add = [
78- # API сервисы
80+ # 2. Seed default data
81+ services_to_seed = [
7982 {
8083 'name' : 'Zadarma' ,
8184 'last_balance' : 0.0 ,
8285 'currency' : SETTINGS .SERVICE_CURRENCIES .get ('Zadarma' , 'USD' ),
8386 },
84- # Wazzup разделён: подписка (ежемесячно) и баланс номера (ежедневный расход)
8587 {
8688 'name' : 'Wazzup24 Подписка' ,
8789 'last_balance' : 0.0 ,
@@ -103,14 +105,12 @@ async def initialize_services(SessionLocal):
103105 'monthly_fee' : SETTINGS .DIDWW_MONTHLY_FEE ,
104106 'next_monthly_alert' : TIMEZONE .localize (datetime (2025 , 12 , 20 , 10 , 0 )),
105107 },
106- # Callii (Управляемый FSM)
107108 {
108109 'name' : 'Callii' ,
109110 'next_alert_date' : TIMEZONE .localize (datetime (2025 , 12 , 11 , 10 , 0 )),
110111 'currency' : SETTINGS .SERVICE_CURRENCIES .get ('Callii' , 'USD' ),
111112 'daily_cost' : SETTINGS .CALLII_DAILY_COST ,
112113 },
113- # Streamtele (Ежемесячное напоминание)
114114 {
115115 'name' : 'Streamtele' ,
116116 'currency' : SETTINGS .SERVICE_CURRENCIES .get ('Streamtele' , 'UAH' ),
@@ -119,10 +119,7 @@ async def initialize_services(SessionLocal):
119119 },
120120 ]
121121
122- from sqlalchemy .future import select # Дополнительный импорт нужен для 'select'
123-
124- for data in services_to_add :
125- # ИСПРАВЛЕНО: Используем select и where для поиска по уникальному полю 'name'
122+ for data in services_to_seed :
126123 stmt = select (Service ).filter (Service .name == data ['name' ])
127124 result = await session .execute (stmt )
128125 exists = result .scalar_one_or_none ()
0 commit comments