|
1 | | -from datetime import datetime |
2 | | -import pytz |
3 | 1 | from sqlalchemy import Column, Integer, String, Float, Boolean, DateTime, text |
4 | | -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, AsyncEngine |
| 2 | +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession |
5 | 3 | from sqlalchemy.orm import sessionmaker, declarative_base |
6 | 4 | from sqlalchemy.future import select |
7 | | - |
| 5 | +from datetime import datetime |
| 6 | +import pytz |
8 | 7 | from config import SETTINGS |
9 | 8 |
|
10 | | -# Define Timezone |
11 | | -TIMEZONE = pytz.timezone('Asia/Makassar') |
| 9 | +TIMEZONE = pytz.timezone('Asia/Makassar') |
12 | 10 |
|
13 | 11 | Base = declarative_base() |
14 | 12 |
|
15 | 13 | class Service(Base): |
16 | | - """Database model representing a tracked service.""" |
17 | 14 | __tablename__ = 'services' |
18 | | - |
19 | 15 | id = Column(Integer, primary_key=True) |
20 | 16 | name = Column(String, unique=True, nullable=False) |
21 | | - |
22 | | - # Balance Monitoring |
23 | 17 | last_balance = Column(Float, default=0.0) |
24 | 18 | low_balance_alert_sent = Column(Boolean, default=False) |
25 | | - |
26 | | - # Financial Configuration |
27 | | - currency = Column(String, default="USD") # USD, RUB, UAH |
28 | | - daily_cost = Column(Float, nullable=True) # Estimated daily cost |
29 | | - monthly_fee = Column(Float, nullable=True) # Fixed monthly fee |
30 | | - |
31 | | - # Alert Schedule |
| 19 | + currency = Column(String, default="USD") |
| 20 | + daily_cost = Column(Float, nullable=True) |
| 21 | + monthly_fee = Column(Float, nullable=True) |
32 | 22 | next_alert_date = Column(DateTime, nullable=True) |
33 | 23 | next_monthly_alert = Column(DateTime, nullable=True) |
| 24 | + last_alert_at = Column(DateTime, nullable=True) |
34 | 25 |
|
35 | 26 | def __repr__(self): |
36 | 27 | return f"<Service(name='{self.name}', balance={self.last_balance})>" |
37 | 28 |
|
38 | | -async def init_db(database_url: str) -> sessionmaker: |
39 | | - """Initialize the database engine and session factory.""" |
| 29 | +async def init_db(database_url: str): |
40 | 30 | engine = create_async_engine(database_url, echo=False) |
41 | 31 | async with engine.begin() as conn: |
42 | 32 | await conn.run_sync(Base.metadata.create_all) |
43 | | - |
44 | | - async_session = sessionmaker( |
45 | | - autocommit=False, |
46 | | - autoflush=False, |
47 | | - bind=engine, |
48 | | - class_=AsyncSession, |
49 | | - expire_on_commit=False, |
50 | | - ) |
51 | | - return async_session |
52 | | - |
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) |
59 | | - pragma_stmt = text("PRAGMA table_info(services)") |
60 | | - result = await session.execute(pragma_stmt) |
61 | | - existing_columns = {row[1] for row in result.fetchall()} |
| 33 | + return sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False) |
62 | 34 |
|
| 35 | +async def initialize_services(SessionLocal): |
| 36 | + async with SessionLocal() as session: |
| 37 | + # Schema migration |
| 38 | + result = await session.execute(text("PRAGMA table_info(services)")) |
| 39 | + columns = {row[1] for row in result.fetchall()} |
63 | 40 | alter_statements = [] |
64 | | - required_columns = ['currency', 'daily_cost', 'monthly_fee', 'next_monthly_alert'] |
65 | 41 |
|
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}") |
70 | | - |
71 | | - for stmt in alter_statements: |
72 | | - try: |
73 | | - await session.execute(text(stmt)) |
74 | | - except Exception: |
75 | | - pass # Ignore errors if column exists |
| 42 | + for col in ['currency', 'daily_cost', 'monthly_fee', 'next_monthly_alert', 'last_alert_at']: |
| 43 | + if col not in columns: |
| 44 | + ctype = 'DATETIME' if 'alert' in col else ('FLOAT' if 'cost' in col or 'fee' in col else 'VARCHAR') |
| 45 | + alter_statements.append(f"ALTER TABLE services ADD COLUMN {col} {ctype}") |
76 | 46 |
|
77 | | - if alter_statements: |
78 | | - await session.commit() |
| 47 | + for stmt in alter_statements: |
| 48 | + try: await session.execute(text(stmt)) |
| 49 | + except Exception: pass |
| 50 | + if alter_statements: await session.commit() |
79 | 51 |
|
80 | | - # 2. Seed default data |
81 | | - services_to_seed = [ |
82 | | - { |
83 | | - 'name': 'Zadarma', |
84 | | - 'last_balance': 0.0, |
85 | | - 'currency': SETTINGS.SERVICE_CURRENCIES.get('Zadarma', 'USD'), |
86 | | - }, |
87 | | - { |
88 | | - 'name': 'Wazzup24 Подписка', |
89 | | - 'last_balance': 0.0, |
90 | | - 'currency': SETTINGS.SERVICE_CURRENCIES.get('Wazzup24 Подписка', 'RUB'), |
91 | | - 'monthly_fee': SETTINGS.WAZZUP_MONTHLY_FEE, |
92 | | - 'next_monthly_alert': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0)), |
93 | | - }, |
94 | | - { |
95 | | - 'name': 'Wazzup24 Баланс номера', |
96 | | - 'last_balance': 0.0, |
97 | | - 'currency': SETTINGS.SERVICE_CURRENCIES.get('Wazzup24 Баланс номера', 'RUB'), |
98 | | - 'daily_cost': SETTINGS.WAZZUP_DAILY_COST, |
99 | | - 'next_alert_date': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0)), |
100 | | - }, |
101 | | - { |
102 | | - 'name': 'DIDWW', |
103 | | - 'last_balance': 0.0, |
104 | | - 'currency': SETTINGS.SERVICE_CURRENCIES.get('DIDWW', 'USD'), |
105 | | - 'monthly_fee': SETTINGS.DIDWW_MONTHLY_FEE, |
106 | | - 'next_monthly_alert': TIMEZONE.localize(datetime(2025, 12, 20, 10, 0)), |
107 | | - }, |
108 | | - { |
109 | | - 'name': 'Callii', |
110 | | - 'next_alert_date': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0)), |
111 | | - 'currency': SETTINGS.SERVICE_CURRENCIES.get('Callii', 'USD'), |
112 | | - 'daily_cost': SETTINGS.CALLII_DAILY_COST, |
113 | | - }, |
114 | | - { |
115 | | - 'name': 'Streamtele', |
116 | | - 'currency': SETTINGS.SERVICE_CURRENCIES.get('Streamtele', 'UAH'), |
117 | | - 'monthly_fee': SETTINGS.STREAMTELE_MONTHLY_FEE, |
118 | | - 'next_monthly_alert': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0)), |
119 | | - }, |
| 52 | + # Seed Data |
| 53 | + services_to_add = [ |
| 54 | + {'name': 'Zadarma', 'last_balance': 0.0, 'currency': 'USD'}, |
| 55 | + {'name': 'DIDWW', 'last_balance': 0.0, 'currency': 'USD', 'monthly_fee': SETTINGS.DIDWW_MONTHLY_FEE, 'next_monthly_alert': TIMEZONE.localize(datetime(2025, 12, 20, 10, 0))}, |
| 56 | + {'name': 'Make', 'last_balance': 0.0, 'currency': 'Ops'}, # NEW |
| 57 | + {'name': 'Wazzup24 Подписка', 'last_balance': 0.0, 'currency': 'RUB', 'monthly_fee': SETTINGS.WAZZUP_MONTHLY_FEE, 'next_monthly_alert': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0))}, |
| 58 | + {'name': 'Wazzup24 Баланс номера', 'last_balance': 0.0, 'currency': 'RUB', 'daily_cost': SETTINGS.WAZZUP_DAILY_COST, 'next_alert_date': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0))}, |
| 59 | + {'name': 'Callii', 'next_alert_date': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0)), 'currency': 'USD', 'daily_cost': SETTINGS.CALLII_DAILY_COST}, |
| 60 | + {'name': 'Streamtele', 'currency': 'UAH', 'monthly_fee': SETTINGS.STREAMTELE_MONTHLY_FEE, 'next_monthly_alert': TIMEZONE.localize(datetime(2025, 12, 11, 10, 0))}, |
120 | 61 | ] |
121 | 62 |
|
122 | | - for data in services_to_seed: |
123 | | - stmt = select(Service).filter(Service.name == data['name']) |
124 | | - result = await session.execute(stmt) |
125 | | - exists = result.scalar_one_or_none() |
126 | | - |
| 63 | + for data in services_to_add: |
| 64 | + stmt = select(Service).where(Service.name == data['name']) |
| 65 | + exists = (await session.execute(stmt)).scalar_one_or_none() |
127 | 66 | if not exists: |
128 | 67 | session.add(Service(name=data['name'], **{k: v for k, v in data.items() if k != 'name'})) |
129 | 68 |
|
|
0 commit comments