feat: multi-Oracle server support with runtime switching

Complete implementation of multi-server Oracle database support:

Backend:
- Multi-pool Oracle with lazy loading per server
- Email-to-server cache for automatic server discovery
- JWT tokens include server_id claim
- /auth/check-identity and /auth/check-email endpoints
- /auth/my-servers endpoint for listing user's accessible servers
- Server switch with password re-authentication

Frontend:
- New ServerSelector component for header dropdown
- Multi-step login flow (identity → server → password)
- Server switching from header with password modal
- Mobile drawer menu with server selection
- Dark mode support for all new components
- URL bookmark support with ?server= query param

Scripts:
- Unified start.sh replacing start-prod.sh/start-test.sh
- Unified ssh-tunnel.sh with multi-server support
- Updated status.sh for new architecture

Tests:
- E2E tests for multi-server and single-server login flows
- Backend unit tests for all new endpoints
- Oracle multi-pool integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-01-26 22:39:06 +00:00
parent 5f99ee2fd0
commit b137e80b71
102 changed files with 9398 additions and 2787 deletions

View File

@@ -1,7 +1,7 @@
"""Nomenclature API endpoints."""
from typing import Optional, List, Annotated
from fastapi import APIRouter, Depends, HTTPException, Header
from fastapi import APIRouter, Depends, HTTPException, Header, Request
from sqlalchemy.ext.asyncio import AsyncSession
from pydantic import BaseModel
@@ -190,14 +190,16 @@ async def get_cash_registers(
@router.post("/sync/suppliers", response_model=SyncResult)
async def sync_suppliers(
request: Request,
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
selected_company: SelectedCompany = None,
):
"""Manually trigger supplier sync from Oracle."""
cid = company_id or selected_company
server_id = getattr(request.state, 'server_id', None)
synced, errors = await SyncService.sync_suppliers(session, cid)
synced, errors = await SyncService.sync_suppliers(session, cid, server_id=server_id)
return SyncResult(
synced=synced,
@@ -208,14 +210,16 @@ async def sync_suppliers(
@router.post("/sync/cash-registers", response_model=SyncResult)
async def sync_cash_registers(
request: Request,
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
selected_company: SelectedCompany = None,
):
"""Manually trigger cash register sync from Oracle."""
cid = company_id or selected_company
server_id = getattr(request.state, 'server_id', None)
synced, errors = await SyncService.sync_cash_registers(session, cid)
synced, errors = await SyncService.sync_cash_registers(session, cid, server_id=server_id)
return SyncResult(
synced=synced,
@@ -226,18 +230,20 @@ async def sync_cash_registers(
@router.post("/sync/all", response_model=dict)
async def sync_all_nomenclatures(
request: Request,
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
selected_company: SelectedCompany = None,
):
"""Sync all nomenclatures (suppliers + cash registers) from Oracle."""
cid = company_id or selected_company
server_id = getattr(request.state, 'server_id', None)
# Sync suppliers
suppliers_synced, suppliers_errors = await SyncService.sync_suppliers(session, cid)
suppliers_synced, suppliers_errors = await SyncService.sync_suppliers(session, cid, server_id=server_id)
# Sync cash registers
registers_synced, registers_errors = await SyncService.sync_cash_registers(session, cid)
registers_synced, registers_errors = await SyncService.sync_cash_registers(session, cid, server_id=server_id)
return {
"suppliers": {

View File

@@ -61,6 +61,9 @@ DEFAULT_FILES_DIR = DEFAULT_QUEUE_DIR / "files"
# Job expiration
JOB_EXPIRY_HOURS = 24
# SQLite busy timeout (milliseconds) - prevents "database is locked" errors
SQLITE_BUSY_TIMEOUT_MS = 5000
class OCRJobStatus(str, Enum):
"""Job status enum."""
@@ -152,6 +155,10 @@ class OCRJobQueue:
# Create database and tables
async with aiosqlite.connect(str(self.db_path)) as db:
# Enable WAL mode for better concurrency and set busy timeout
await db.execute("PRAGMA journal_mode=WAL")
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
await db.execute('''
CREATE TABLE IF NOT EXISTS ocr_jobs (
id TEXT PRIMARY KEY,
@@ -262,6 +269,7 @@ class OCRJobQueue:
# Insert job record
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
await db.execute('''
INSERT INTO ocr_jobs (
id, status, file_path, mime_type, engine,
@@ -302,6 +310,7 @@ class OCRJobQueue:
await self.initialize()
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
db.row_factory = aiosqlite.Row
async with db.execute(
'SELECT * FROM ocr_jobs WHERE id = ?',
@@ -325,6 +334,7 @@ class OCRJobQueue:
await self.initialize()
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
# Check if job is pending
async with db.execute(
'SELECT status, created_at FROM ocr_jobs WHERE id = ?',
@@ -359,6 +369,7 @@ class OCRJobQueue:
async with self._lock: # Serialize access to prevent race conditions
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
db.row_factory = aiosqlite.Row
# Get the next pending job
@@ -451,6 +462,7 @@ class OCRJobQueue:
params = (status.value, job_id)
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
cursor = await db.execute(query, params)
await db.commit()
return cursor.rowcount > 0
@@ -467,6 +479,7 @@ class OCRJobQueue:
await self.initialize()
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
async with db.execute('''
SELECT AVG(processing_time_ms)
FROM (
@@ -486,6 +499,7 @@ class OCRJobQueue:
await self.initialize()
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
async with db.execute(
'SELECT COUNT(*) FROM ocr_jobs WHERE status = ?',
(OCRJobStatus.pending.value,)
@@ -498,6 +512,7 @@ class OCRJobQueue:
await self.initialize()
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
async with db.execute(
'SELECT COUNT(*) FROM ocr_jobs WHERE status = ?',
(OCRJobStatus.processing.value,)
@@ -518,6 +533,7 @@ class OCRJobQueue:
deleted = 0
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
db.row_factory = aiosqlite.Row
# Get expired jobs
@@ -588,6 +604,7 @@ class OCRJobQueue:
}
async with aiosqlite.connect(str(self.db_path)) as db:
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
async with db.execute('''
SELECT status, COUNT(*) as count
FROM ocr_jobs

View File

@@ -19,24 +19,30 @@ from backend.modules.data_entry.db.models.nomenclature import SyncedSupplier, Lo
logger = logging.getLogger(__name__)
# Cache for schema lookups (populated dynamically from Oracle)
_schema_cache: dict[int, str] = {}
# Key format: (server_id, company_id) for multi-server support
_schema_cache: dict[tuple, str] = {}
class SyncService:
"""Service for syncing nomenclatures from Oracle."""
@staticmethod
async def get_schema_for_company(company_id: int) -> Optional[str]:
async def get_schema_for_company(company_id: int, server_id: Optional[str] = None) -> Optional[str]:
"""
Get Oracle schema for company ID from V_NOM_FIRME view.
Results are cached in memory for performance.
Args:
company_id: The company ID to look up
server_id: Optional Oracle server ID for multi-server mode
"""
# Check cache first
if company_id in _schema_cache:
return _schema_cache[company_id]
# Check cache first - use (server_id, company_id) as key for multi-server support
cache_key = (server_id, company_id)
if cache_key in _schema_cache:
return _schema_cache[cache_key]
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
cursor.execute("""
SELECT SCHEMA
@@ -47,34 +53,39 @@ class SyncService:
if result:
schema = result[0]
_schema_cache[company_id] = schema
logger.info(f"Resolved schema for company {company_id}: {schema}")
_schema_cache[cache_key] = schema
logger.info(f"Resolved schema for company {company_id} on server {server_id}: {schema}")
return schema
else:
logger.warning(f"No schema found for company {company_id}")
logger.warning(f"No schema found for company {company_id} on server {server_id}")
return None
except Exception as e:
logger.error(f"Error fetching schema for company {company_id}: {e}")
logger.error(f"Error fetching schema for company {company_id} on server {server_id}: {e}")
return None
@staticmethod
async def sync_suppliers(session: AsyncSession, company_id: int) -> Tuple[int, int]:
async def sync_suppliers(session: AsyncSession, company_id: int, server_id: Optional[str] = None) -> Tuple[int, int]:
"""
Sync suppliers (furnizori, id_tip_part=17) from Oracle to SQLite.
Uses CORESP_TIP_PART joined with VNOM_PARTENERI view.
Returns (synced_count, error_count).
Args:
session: SQLAlchemy async session for SQLite
company_id: The company ID to sync suppliers for
server_id: Optional Oracle server ID for multi-server mode
"""
schema = await SyncService.get_schema_for_company(company_id)
schema = await SyncService.get_schema_for_company(company_id, server_id)
if not schema:
logger.warning(f"No schema mapping for company {company_id}")
logger.warning(f"No schema mapping for company {company_id} on server {server_id}")
return 0, 0
synced = 0
errors = 0
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Fetch active suppliers from Oracle
# id_tip_part = 17 means "furnizori" (suppliers)
@@ -139,7 +150,7 @@ class SyncService:
return synced, errors
@staticmethod
async def sync_cash_registers(session: AsyncSession, company_id: int) -> Tuple[int, int]:
async def sync_cash_registers(session: AsyncSession, company_id: int, server_id: Optional[str] = None) -> Tuple[int, int]:
"""
Sync cash registers and bank accounts from Oracle to SQLite.
Returns (synced_count, error_count).
@@ -149,10 +160,15 @@ class SyncService:
- id_tip_part = 23: CASA VALUTA
- id_tip_part = 24: BANCA LEI
- id_tip_part = 25: BANCA VALUTA
Args:
session: SQLAlchemy async session for SQLite
company_id: The company ID to sync cash registers for
server_id: Optional Oracle server ID for multi-server mode
"""
schema = await SyncService.get_schema_for_company(company_id)
schema = await SyncService.get_schema_for_company(company_id, server_id)
if not schema:
logger.warning(f"No schema mapping for company {company_id}")
logger.warning(f"No schema mapping for company {company_id} on server {server_id}")
return 0, 0
synced = 0
@@ -164,7 +180,7 @@ class SyncService:
partner_types = [22, 23, 24, 25]
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Fetch cash/bank partners from CORESP_TIP_PART
cursor.execute(f"""

View File

@@ -62,6 +62,10 @@ class CacheManager:
except asyncio.CancelledError:
pass
# Close SQLite connection manager
if hasattr(self.sqlite, 'close'):
await self.sqlite.close()
logger.info("Cache closed")
async def get(self, key: str, cache_type: str) -> Optional[Any]:

View File

@@ -3,6 +3,8 @@ Cache decorators for service methods
"""
import time
import logging
import sqlite3
import asyncio
from functools import wraps
from typing import Callable, Optional, List
@@ -11,6 +13,10 @@ from .keys import generate_cache_key
logger = logging.getLogger(__name__)
# Retry configuration for SQLite locked database errors
SQLITE_MAX_RETRIES = 3
SQLITE_RETRY_BASE_DELAY = 0.1 # 100ms base delay, exponential backoff
def cached(cache_type: str, ttl: Optional[int] = None, key_params: Optional[List[str]] = None):
"""
@@ -73,8 +79,21 @@ def cached(cache_type: str, ttl: Optional[int] = None, key_params: Optional[List
# Generate cache key from function parameters
cache_key = generate_cache_key(cache_type, key_params, args, kwargs)
# Try to get from cache
cached_value = await cache.get(cache_key, cache_type)
# Try to get from cache with retry logic for SQLite locks
cached_value = None
for attempt in range(SQLITE_MAX_RETRIES):
try:
cached_value = await cache.get(cache_key, cache_type)
break
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < SQLITE_MAX_RETRIES - 1:
delay = SQLITE_RETRY_BASE_DELAY * (attempt + 1)
logger.warning(f"SQLite locked on cache.get, retry {attempt + 1}/{SQLITE_MAX_RETRIES} after {delay}s")
await asyncio.sleep(delay)
else:
logger.error(f"SQLite error after {attempt + 1} retries: {e}")
cached_value = None
break
if cached_value is not None:
# ✅ CACHE HIT
@@ -128,9 +147,21 @@ def cached(cache_type: str, ttl: Optional[int] = None, key_params: Optional[List
username=username
)
# Store in cache for next time
# Store in cache for next time (with retry logic for SQLite locks)
company_id = _extract_company_id(args, kwargs, key_params)
await cache.set(cache_key, result, cache_type, company_id, ttl)
for attempt in range(SQLITE_MAX_RETRIES):
try:
await cache.set(cache_key, result, cache_type, company_id, ttl)
break
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < SQLITE_MAX_RETRIES - 1:
delay = SQLITE_RETRY_BASE_DELAY * (attempt + 1)
logger.warning(f"SQLite locked on cache.set, retry {attempt + 1}/{SQLITE_MAX_RETRIES} after {delay}s")
await asyncio.sleep(delay)
else:
logger.error(f"SQLite error on cache.set after {attempt + 1} retries: {e}")
# Don't fail the request, just skip caching
break
return result

View File

@@ -1,16 +1,23 @@
"""
SQLite persistent cache (L2 cache)
Persistent, survives restarts, unlimited size
Uses singleton connection pattern with asyncio.Lock for write serialization
to prevent "database is locked" errors under concurrent access.
"""
import time
import json
import logging
import asyncio
import aiosqlite
from typing import Any, Optional, List, Dict
from pathlib import Path
from decimal import Decimal
from datetime import datetime, date
# SQLite busy timeout in milliseconds (wait for lock instead of failing immediately)
SQLITE_BUSY_TIMEOUT_MS = 5000
logger = logging.getLogger(__name__)
@@ -31,6 +38,163 @@ class CustomJSONEncoder(json.JSONEncoder):
return super().default(obj)
class SQLiteConnectionManager:
"""
Singleton connection manager with write serialization.
Solves "database is locked" errors by:
1. Maintaining a single persistent connection (instead of N connections per request)
2. Serializing all write operations through an asyncio.Lock
3. Using WAL mode for better concurrent read performance
Architecture:
┌─────────────────────────────────────┐
│ SQLiteConnectionManager │
│ (SINGLETON) │
│ │
│ _connection: aiosqlite.Connection │
│ _write_lock: asyncio.Lock │
└─────────────────────────────────────┘
┌───────────────┼───────────────┐
▼ ▼ ▼
Task 1 Task 2 Task N
cache.get() cache.set() cache.get()
│ │ │
└───────────────┴───────────────┘
async with _write_lock:
(serialized writes)
"""
_instance: Optional['SQLiteConnectionManager'] = None
_instance_lock: asyncio.Lock = None # Will be created on first use
def __init__(self, db_path: str):
"""
Initialize connection manager (called only by get_instance).
Args:
db_path: Path to SQLite database file
"""
self.db_path = db_path
self._connection: Optional[aiosqlite.Connection] = None
self._write_lock: Optional[asyncio.Lock] = None
self._initialized = False
@classmethod
async def get_instance(cls, db_path: str) -> 'SQLiteConnectionManager':
"""
Get or create singleton instance.
Thread-safe singleton pattern using asyncio.Lock.
Args:
db_path: Path to SQLite database file
Returns:
SQLiteConnectionManager singleton instance
"""
# Create instance lock on first call (must be done in async context)
if cls._instance_lock is None:
cls._instance_lock = asyncio.Lock()
async with cls._instance_lock:
if cls._instance is None or cls._instance.db_path != db_path:
cls._instance = cls(db_path)
return cls._instance
async def initialize(self):
"""
Create connection with WAL mode and busy timeout.
Sets up:
- Busy timeout (5 seconds) - wait for locks instead of failing
- WAL journal mode - allows concurrent reads while writing
- Write lock for serializing write operations
"""
if self._initialized:
return
# Create write lock in async context
self._write_lock = asyncio.Lock()
# Create persistent connection
self._connection = await aiosqlite.connect(self.db_path)
await self._connection.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
await self._connection.execute("PRAGMA journal_mode=WAL")
await self._connection.commit()
self._initialized = True
logger.info(f"SQLite connection manager initialized: {self.db_path}")
async def get_connection(self) -> aiosqlite.Connection:
"""
Get the persistent connection, with health check.
If connection is unhealthy (closed or stale), reconnects automatically.
Returns:
Active aiosqlite connection
"""
if self._connection is None or not await self._is_healthy():
await self._reconnect()
return self._connection
async def _is_healthy(self) -> bool:
"""
Check if connection is valid.
Returns:
True if connection can execute queries, False otherwise
"""
try:
async with self._connection.execute("SELECT 1") as cursor:
await cursor.fetchone()
return True
except Exception:
return False
async def _reconnect(self):
"""Reconnect if connection was lost."""
logger.warning("SQLite connection unhealthy, reconnecting...")
# Close old connection if exists
if self._connection:
try:
await self._connection.close()
except Exception:
pass
# Create new connection
self._connection = await aiosqlite.connect(self.db_path)
await self._connection.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
await self._connection.execute("PRAGMA journal_mode=WAL")
await self._connection.commit()
logger.info("SQLite connection re-established")
@property
def write_lock(self) -> asyncio.Lock:
"""Get the write lock for serializing write operations."""
return self._write_lock
async def close(self):
"""Close the connection and reset singleton."""
if self._connection:
try:
await self._connection.close()
except Exception as e:
logger.warning(f"Error closing SQLite connection: {e}")
self._connection = None
self._initialized = False
# Reset singleton
SQLiteConnectionManager._instance = None
logger.info("SQLite connection manager closed")
class SQLiteCache:
"""
SQLite-based persistent cache
@@ -41,6 +205,7 @@ class SQLiteCache:
- Schema mappings (permanent cache for company->schema)
- Watermarks for event-based invalidation
- Performance tracking and benchmarks
- Singleton connection with write serialization (prevents "database is locked")
"""
def __init__(self, db_path: str):
@@ -51,6 +216,7 @@ class SQLiteCache:
db_path: Path to SQLite database file
"""
self.db_path = db_path
self._conn_manager: Optional[SQLiteConnectionManager] = None
self._ensure_db_dir()
def _ensure_db_dir(self):
@@ -60,13 +226,16 @@ class SQLiteCache:
async def init_db(self):
"""Initialize database schema with WAL mode enabled"""
async with aiosqlite.connect(self.db_path) as db:
# Enable Write-Ahead Logging (WAL) mode for better concurrency
await db.execute("PRAGMA journal_mode=WAL")
await db.commit()
# Get or create singleton connection manager
self._conn_manager = await SQLiteConnectionManager.get_instance(self.db_path)
await self._conn_manager.initialize()
# Create tables using the persistent connection
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
# Table: cache_entries
await db.execute("""
await conn.execute("""
CREATE TABLE IF NOT EXISTS cache_entries (
cache_key TEXT PRIMARY KEY,
cache_type TEXT NOT NULL,
@@ -78,12 +247,12 @@ class SQLiteCache:
last_accessed REAL
)
""")
await db.execute("CREATE INDEX IF NOT EXISTS idx_cache_type ON cache_entries(cache_type)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_company_id ON cache_entries(company_id)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_expires_at ON cache_entries(expires_at)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_cache_type ON cache_entries(cache_type)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_company_id ON cache_entries(company_id)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_expires_at ON cache_entries(expires_at)")
# Table: schema_mappings (PERMANENT)
await db.execute("""
await conn.execute("""
CREATE TABLE IF NOT EXISTS schema_mappings (
id_firma INTEGER PRIMARY KEY,
schema TEXT NOT NULL,
@@ -93,7 +262,7 @@ class SQLiteCache:
""")
# Table: query_benchmarks
await db.execute("""
await conn.execute("""
CREATE TABLE IF NOT EXISTS query_benchmarks (
cache_type TEXT PRIMARY KEY,
avg_time_ms REAL NOT NULL,
@@ -103,7 +272,7 @@ class SQLiteCache:
""")
# Table: performance_log
await db.execute("""
await conn.execute("""
CREATE TABLE IF NOT EXISTS performance_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cache_type TEXT NOT NULL,
@@ -116,11 +285,11 @@ class SQLiteCache:
timestamp REAL NOT NULL
)
""")
await db.execute("CREATE INDEX IF NOT EXISTS idx_perf_timestamp ON performance_log(timestamp)")
await db.execute("CREATE INDEX IF NOT EXISTS idx_perf_cache_type ON performance_log(cache_type)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_perf_timestamp ON performance_log(timestamp)")
await conn.execute("CREATE INDEX IF NOT EXISTS idx_perf_cache_type ON performance_log(cache_type)")
# Table: user_cache_settings
await db.execute("""
await conn.execute("""
CREATE TABLE IF NOT EXISTS user_cache_settings (
username TEXT PRIMARY KEY,
cache_enabled BOOLEAN DEFAULT TRUE,
@@ -130,7 +299,7 @@ class SQLiteCache:
""")
# Table: cache_config
await db.execute("""
await conn.execute("""
CREATE TABLE IF NOT EXISTS cache_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
@@ -139,7 +308,7 @@ class SQLiteCache:
""")
# Table: cache_watermarks
await db.execute("""
await conn.execute("""
CREATE TABLE IF NOT EXISTS cache_watermarks (
company_id INTEGER PRIMARY KEY,
schema TEXT NOT NULL,
@@ -148,7 +317,7 @@ class SQLiteCache:
)
""")
await db.commit()
await conn.commit()
logger.info("SQLite cache database initialized")
async def get(self, key: str) -> Optional[Any]:
@@ -161,8 +330,11 @@ class SQLiteCache:
Returns:
Cached value or None if not found/expired
"""
async with aiosqlite.connect(self.db_path) as db:
async with db.execute("""
# Use write lock because we may update hit_count or delete expired entries
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
async with conn.execute("""
SELECT data_json, expires_at
FROM cache_entries
WHERE cache_key = ?
@@ -177,18 +349,18 @@ class SQLiteCache:
# Check TTL expiration
if expires_at < time.time():
# Expired - delete and return None
await db.execute("DELETE FROM cache_entries WHERE cache_key = ?", (key,))
await db.commit()
await conn.execute("DELETE FROM cache_entries WHERE cache_key = ?", (key,))
await conn.commit()
logger.debug(f"SQLite cache expired: {key}")
return None
# Update hit_count and last_accessed
await db.execute("""
await conn.execute("""
UPDATE cache_entries
SET hit_count = hit_count + 1, last_accessed = ?
WHERE cache_key = ?
""", (time.time(), key))
await db.commit()
await conn.commit()
logger.debug(f"SQLite cache HIT: {key}")
return json.loads(data_json)
@@ -209,21 +381,23 @@ class SQLiteCache:
now = time.time()
expires_at = now + ttl
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
await conn.execute("""
INSERT OR REPLACE INTO cache_entries
(cache_key, cache_type, company_id, data_json, created_at, expires_at, hit_count, last_accessed)
VALUES (?, ?, ?, ?, ?, ?, 0, ?)
""", (key, cache_type, company_id, data_json, now, expires_at, now))
await db.commit()
await conn.commit()
logger.debug(f"SQLite cache SET: {key} (TTL: {ttl}s)")
async def delete(self, key: str) -> bool:
"""Delete entry from cache"""
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute("DELETE FROM cache_entries WHERE cache_key = ?", (key,))
await db.commit()
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
cursor = await conn.execute("DELETE FROM cache_entries WHERE cache_key = ?", (key,))
await conn.commit()
deleted = cursor.rowcount > 0
if deleted:
logger.debug(f"SQLite cache deleted: {key}")
@@ -231,33 +405,37 @@ class SQLiteCache:
async def clear(self):
"""Clear all cache entries"""
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute("DELETE FROM cache_entries")
await db.commit()
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
cursor = await conn.execute("DELETE FROM cache_entries")
await conn.commit()
count = cursor.rowcount
logger.info(f"SQLite cache cleared: {count} entries removed")
async def clear_by_company(self, company_id: int):
"""Clear all entries for specific company"""
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute("DELETE FROM cache_entries WHERE company_id = ?", (company_id,))
await db.commit()
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
cursor = await conn.execute("DELETE FROM cache_entries WHERE company_id = ?", (company_id,))
await conn.commit()
count = cursor.rowcount
logger.info(f"SQLite cache cleared for company {company_id}: {count} entries")
async def clear_by_type(self, cache_type: str):
"""Clear all entries of specific type"""
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute("DELETE FROM cache_entries WHERE cache_type = ?", (cache_type,))
await db.commit()
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
cursor = await conn.execute("DELETE FROM cache_entries WHERE cache_type = ?", (cache_type,))
await conn.commit()
count = cursor.rowcount
logger.info(f"SQLite cache cleared for type '{cache_type}': {count} entries")
async def cleanup_expired(self):
"""Remove all expired entries"""
async with aiosqlite.connect(self.db_path) as db:
cursor = await db.execute("DELETE FROM cache_entries WHERE expires_at < ?", (time.time(),))
await db.commit()
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
cursor = await conn.execute("DELETE FROM cache_entries WHERE expires_at < ?", (time.time(),))
await conn.commit()
count = cursor.rowcount
if count > 0:
logger.info(f"SQLite cache cleanup: {count} expired entries removed")
@@ -265,48 +443,50 @@ class SQLiteCache:
# Schema Mappings (PERMANENT)
async def get_schema_mapping(self, company_id: int) -> Optional[str]:
"""Get permanent cached schema for company"""
async with aiosqlite.connect(self.db_path) as db:
async with db.execute("""
SELECT schema
FROM schema_mappings
WHERE id_firma = ?
""", (company_id,)) as cursor:
result = await cursor.fetchone()
return result[0] if result else None
"""Get permanent cached schema for company (READ-ONLY, no lock needed)"""
conn = await self._conn_manager.get_connection()
async with conn.execute("""
SELECT schema
FROM schema_mappings
WHERE id_firma = ?
""", (company_id,)) as cursor:
result = await cursor.fetchone()
return result[0] if result else None
async def set_schema_mapping(self, company_id: int, schema: str):
"""Set permanent schema mapping (never expires)"""
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
await conn.execute("""
INSERT OR REPLACE INTO schema_mappings
(id_firma, schema, created_at, last_verified)
VALUES (?, ?, ?, ?)
""", (company_id, schema, time.time(), time.time()))
await db.commit()
await conn.commit()
# Benchmarks
async def get_benchmark(self, cache_type: str) -> Optional[float]:
"""Get average benchmark time for cache type"""
async with aiosqlite.connect(self.db_path) as db:
async with db.execute("""
SELECT avg_time_ms
FROM query_benchmarks
WHERE cache_type = ?
""", (cache_type,)) as cursor:
result = await cursor.fetchone()
return result[0] if result else None
"""Get average benchmark time for cache type (READ-ONLY, no lock needed)"""
conn = await self._conn_manager.get_connection()
async with conn.execute("""
SELECT avg_time_ms
FROM query_benchmarks
WHERE cache_type = ?
""", (cache_type,)) as cursor:
result = await cursor.fetchone()
return result[0] if result else None
async def set_benchmark(self, cache_type: str, avg_time_ms: float, sample_count: int):
"""Set/update benchmark"""
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
await conn.execute("""
INSERT OR REPLACE INTO query_benchmarks
(cache_type, avg_time_ms, sample_count, last_updated)
VALUES (?, ?, ?, ?)
""", (cache_type, avg_time_ms, sample_count, time.time()))
await db.commit()
await conn.commit()
# Performance Tracking
@@ -314,91 +494,101 @@ class SQLiteCache:
response_time_ms: float, estimated_oracle_time_ms: Optional[float],
time_saved_ms: Optional[float], username: Optional[str]):
"""Log performance metric"""
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
await conn.execute("""
INSERT INTO performance_log
(cache_type, company_id, cache_hit, response_time_ms, estimated_oracle_time_ms,
time_saved_ms, username, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (cache_type, company_id, cache_hit, response_time_ms, estimated_oracle_time_ms,
time_saved_ms, username, time.time()))
await db.commit()
await conn.commit()
# User Settings
async def get_user_cache_enabled(self, username: str) -> bool:
"""Get user cache setting (default True)"""
async with aiosqlite.connect(self.db_path) as db:
async with db.execute("""
SELECT cache_enabled
FROM user_cache_settings
WHERE username = ?
""", (username,)) as cursor:
result = await cursor.fetchone()
return bool(result[0]) if result else True # Default enabled, explicit bool conversion
"""Get user cache setting (default True) - READ-ONLY, no lock needed"""
conn = await self._conn_manager.get_connection()
async with conn.execute("""
SELECT cache_enabled
FROM user_cache_settings
WHERE username = ?
""", (username,)) as cursor:
result = await cursor.fetchone()
return bool(result[0]) if result else True # Default enabled, explicit bool conversion
async def set_user_cache_enabled(self, username: str, enabled: bool):
"""Set user cache setting"""
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
await conn.execute("""
INSERT OR REPLACE INTO user_cache_settings
(username, cache_enabled, created_at, updated_at)
VALUES (?, ?, ?, ?)
""", (username, enabled, time.time(), time.time()))
await db.commit()
await conn.commit()
# Watermarks
async def get_watermark(self, company_id: int) -> Optional[int]:
"""Get cached watermark (max_id_act) for company"""
async with aiosqlite.connect(self.db_path) as db:
async with db.execute("""
SELECT max_id_act
FROM cache_watermarks
WHERE company_id = ?
""", (company_id,)) as cursor:
result = await cursor.fetchone()
return result[0] if result else None
"""Get cached watermark (max_id_act) for company - READ-ONLY, no lock needed"""
conn = await self._conn_manager.get_connection()
async with conn.execute("""
SELECT max_id_act
FROM cache_watermarks
WHERE company_id = ?
""", (company_id,)) as cursor:
result = await cursor.fetchone()
return result[0] if result else None
async def set_watermark(self, company_id: int, schema: str, max_id_act: int):
"""Set/update watermark for company"""
async with aiosqlite.connect(self.db_path) as db:
await db.execute("""
async with self._conn_manager.write_lock:
conn = await self._conn_manager.get_connection()
await conn.execute("""
INSERT OR REPLACE INTO cache_watermarks
(company_id, schema, max_id_act, checked_at)
VALUES (?, ?, ?, ?)
""", (company_id, schema, max_id_act, time.time()))
await db.commit()
await conn.commit()
async def get_cached_company_ids(self) -> List[int]:
"""Get list of company_ids with active cache entries"""
async with aiosqlite.connect(self.db_path) as db:
async with db.execute("""
SELECT DISTINCT company_id
FROM cache_entries
WHERE company_id IS NOT NULL
AND expires_at > ?
""", (time.time(),)) as cursor:
results = await cursor.fetchall()
return [row[0] for row in results]
"""Get list of company_ids with active cache entries - READ-ONLY, no lock needed"""
conn = await self._conn_manager.get_connection()
async with conn.execute("""
SELECT DISTINCT company_id
FROM cache_entries
WHERE company_id IS NOT NULL
AND expires_at > ?
""", (time.time(),)) as cursor:
results = await cursor.fetchall()
return [row[0] for row in results]
# Statistics
async def get_stats(self) -> Dict[str, Any]:
"""Get cache statistics"""
async with aiosqlite.connect(self.db_path) as db:
# Total entries
async with db.execute("SELECT COUNT(*) FROM cache_entries") as cursor:
total_entries = (await cursor.fetchone())[0]
"""Get cache statistics - READ-ONLY, no lock needed"""
conn = await self._conn_manager.get_connection()
# Active entries (not expired)
async with db.execute("""
SELECT COUNT(*) FROM cache_entries WHERE expires_at > ?
""", (time.time(),)) as cursor:
active_entries = (await cursor.fetchone())[0]
# Total entries
async with conn.execute("SELECT COUNT(*) FROM cache_entries") as cursor:
total_entries = (await cursor.fetchone())[0]
return {
'total_entries': total_entries,
'active_entries': active_entries,
'expired_entries': total_entries - active_entries
}
# Active entries (not expired)
async with conn.execute("""
SELECT COUNT(*) FROM cache_entries WHERE expires_at > ?
""", (time.time(),)) as cursor:
active_entries = (await cursor.fetchone())[0]
return {
'total_entries': total_entries,
'active_entries': active_entries,
'expired_entries': total_entries - active_entries
}
async def close(self):
"""Close the connection manager"""
if self._conn_manager:
await self._conn_manager.close()
self._conn_manager = None

View File

@@ -36,7 +36,8 @@ async def get_dashboard_summary(
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_complete_summary(company, current_user.username, luna=luna, an=an, request=request)
server_id = getattr(request.state, 'server_id', None)
result = await DashboardService.get_complete_summary(company, current_user.username, luna=luna, an=an, request=request, server_id=server_id)
# Convert Pydantic model to dict for JSON serialization
result_dict = result.dict() if hasattr(result, 'dict') else result
@@ -91,8 +92,9 @@ async def get_dashboard_trends(
detail=f"Perioadă nevalidă: {period}. Valori permise: {', '.join(valid_periods)}"
)
server_id = getattr(request.state, 'server_id', None)
# Obține datele de trenduri
result = await DashboardService.get_trends(int(company), period, luna=luna, an=an, request=request)
result = await DashboardService.get_trends(int(company), period, luna=luna, an=an, request=request, server_id=server_id)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
@@ -120,6 +122,7 @@ async def get_dashboard_trends(
@router.get("/detailed-data")
async def get_detailed_data(
request: Request,
company: str = Query(description="Codul firmei"),
data_type: str = Query(description="Tipul de date: clients, suppliers, treasury"),
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
@@ -137,6 +140,7 @@ async def get_detailed_data(
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
server_id = getattr(request.state, 'server_id', None)
logger.info(f"[ROUTER] Calling DashboardService.get_detailed_data")
result = await DashboardService.get_detailed_data(
company=company,
@@ -145,7 +149,8 @@ async def get_detailed_data(
an=an,
page=page,
page_size=page_size,
search=search
search=search,
server_id=server_id
)
logger.info(f"[ROUTER] Service returned: {len(result.get('data', []))} rows")
@@ -157,13 +162,14 @@ async def get_detailed_data(
@router.get("/performance")
async def get_performance(
request: Request,
company: int = Query(..., description="ID-ul firmei"),
period: str = Query("7d", regex="^(7d|1m|3m|6m|ytd|12m)$", description="Perioada pentru analiză"),
current_user: CurrentUser = Depends(get_current_user)
):
"""
Returnează date performanță pentru perioada selectată
- Necesită autentificare JWT
- Returnează grafice încasări vs plăți pentru perioada selectată
- Calculează indicatori: rata încasării, cash conversion, working capital
@@ -172,8 +178,9 @@ async def get_performance(
# Verifică dacă utilizatorul are acces la firma specificată
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_performance_data(company, period)
server_id = getattr(request.state, 'server_id', None)
result = await DashboardService.get_performance_data(company, period, server_id=server_id)
# Convert to Chart.js compatible format
return {
@@ -195,13 +202,14 @@ async def get_performance(
@router.get("/cashflow")
async def get_cashflow(
request: Request,
company: int = Query(..., description="ID-ul firmei"),
period: str = Query("7d", regex="^(7d|1m|3m|6m)$", description="Perioada pentru previziune"),
current_user: CurrentUser = Depends(get_current_user)
):
"""
Returnează previziune cash flow pentru perioada selectată
- Necesită autentificare JWT
- Analizează scadențele viitoare pentru calculul cash flow-ului
- Identifică zilele critice cu deficit de cash
@@ -210,8 +218,9 @@ async def get_cashflow(
# Verifică dacă utilizatorul are acces la firma specificată
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_cashflow_forecast(company, period)
server_id = getattr(request.state, 'server_id', None)
result = await DashboardService.get_cashflow_forecast(company, period, server_id=server_id)
# Convert to Chart.js compatible format
return {
@@ -263,7 +272,8 @@ async def get_maturity_analysis(
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_maturity_analysis(company, period, luna=luna, an=an, request=request)
server_id = getattr(request.state, 'server_id', None)
result = await DashboardService.get_maturity_analysis(company, period, luna=luna, an=an, request=request, server_id=server_id)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
@@ -308,8 +318,9 @@ async def get_monthly_flows(
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
server_id = getattr(request.state, 'server_id', None)
# Apelăm serviciul cu request pentru cache metadata
result = await DashboardService.get_monthly_flows(company, luna=luna, an=an, request=request)
result = await DashboardService.get_monthly_flows(company, luna=luna, an=an, request=request, server_id=server_id)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
@@ -353,7 +364,8 @@ async def get_treasury_breakdown(
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_treasury_breakdown(company, luna=luna, an=an, request=request)
server_id = getattr(request.state, 'server_id', None)
result = await DashboardService.get_treasury_breakdown(company, luna=luna, an=an, request=request, server_id=server_id)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
@@ -398,7 +410,8 @@ async def get_net_balance_breakdown(
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_net_balance_breakdown(company, luna=luna, an=an, request=request)
server_id = getattr(request.state, 'server_id', None)
result = await DashboardService.get_net_balance_breakdown(company, luna=luna, an=an, request=request, server_id=server_id)
# Convert to dict if needed
result_dict = result.dict() if hasattr(result, 'dict') else result
@@ -424,6 +437,7 @@ async def get_net_balance_breakdown(
@router.get("/current-period")
async def get_current_period(
request: Request,
company: int = Query(..., description="ID-ul firmei"),
current_user: CurrentUser = Depends(get_current_user)
):
@@ -439,7 +453,8 @@ async def get_current_period(
if str(company) not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await DashboardService.get_current_period(company)
server_id = getattr(request.state, 'server_id', None)
result = await DashboardService.get_current_period(company, server_id=server_id)
return result
except ValueError as e:
@@ -502,9 +517,11 @@ async def get_financial_indicators(
resolved_luna: int
resolved_an: int
server_id = getattr(request.state, 'server_id', None)
if luna is None or an is None:
try:
current_period = await DashboardService.get_current_period(company)
current_period = await DashboardService.get_current_period(company, server_id=server_id)
resolved_luna = luna if luna is not None else current_period.get('luna', 12)
resolved_an = an if an is not None else current_period.get('an', 2024)
except Exception as e:
@@ -519,13 +536,22 @@ async def get_financial_indicators(
# Dacă include_sparklines este True, folosim metoda care include datele istorice
if include_sparklines:
response = await FinancialIndicatorsService.get_indicators_with_sparklines(
company, resolved_luna, resolved_an, months=12, request=request
company, resolved_luna, resolved_an, months=12, request=request, server_id=server_id
)
# FIX: Cache poate returna dict în loc de obiect Pydantic
# Extragem valorile pentru logging în mod compatibil cu ambele tipuri
if isinstance(response, dict):
zscore_val = response.get('altman_zscore', {}).get('zscore', {}).get('value')
zscore_status = response.get('altman_zscore', {}).get('zscore', {}).get('status')
else:
zscore_val = response.altman_zscore.zscore.value
zscore_status = response.altman_zscore.zscore.status
logger.info(
f"Financial indicators with sparklines for company {company}, "
f"luna={resolved_luna}, an={resolved_an}: "
f"Z-Score={response.altman_zscore.zscore.value} ({response.altman_zscore.zscore.status}), "
f"Z-Score={zscore_val} ({zscore_status}), "
f"cache_hit={getattr(request.state, 'cache_hit', False)}, "
f"response_time={getattr(request.state, 'response_time_ms', 0):.1f}ms"
)
@@ -545,28 +571,28 @@ async def get_financial_indicators(
# Apelăm serviciul pentru fiecare categorie de indicatori
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
company, resolved_luna, resolved_an
company, resolved_luna, resolved_an, server_id=server_id
)
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
company, resolved_luna, resolved_an
company, resolved_luna, resolved_an, server_id=server_id
)
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
company, resolved_luna, resolved_an
company, resolved_luna, resolved_an, server_id=server_id
)
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
company, resolved_luna, resolved_an
company, resolved_luna, resolved_an, server_id=server_id
)
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
company, resolved_luna, resolved_an
company, resolved_luna, resolved_an, server_id=server_id
)
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
company, resolved_luna, resolved_an
company, resolved_luna, resolved_an, server_id=server_id
)
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
company, resolved_luna, resolved_an
company, resolved_luna, resolved_an, server_id=server_id
)
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
company, resolved_luna, resolved_an
company, resolved_luna, resolved_an, server_id=server_id
)
# Executăm toate calculele în paralel pentru performanță
@@ -602,9 +628,17 @@ async def get_financial_indicators(
solvabilitate=solvabilitate
)
# FIX: Cache poate returna dict în loc de obiect Pydantic
if isinstance(altman_zscore, dict):
zscore_val = altman_zscore.get('zscore', {}).get('value')
zscore_status = altman_zscore.get('zscore', {}).get('status')
else:
zscore_val = altman_zscore.zscore.value
zscore_status = altman_zscore.zscore.status
logger.info(
f"Financial indicators for company {company}, luna={resolved_luna}, an={resolved_an}: "
f"Z-Score={altman_zscore.zscore.value} ({altman_zscore.zscore.status})"
f"Z-Score={zscore_val} ({zscore_status})"
)
# Add cache metadata if requested (for Telegram Bot / Dashboard)

View File

@@ -1,7 +1,7 @@
"""
API Router pentru facturi
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from typing import List, Optional
from datetime import date
# import sys # Removed - no longer needed
@@ -16,6 +16,7 @@ router = APIRouter()
@router.get("/", response_model=InvoiceListResponse)
async def get_invoices(
request: Request,
company: str = Query(description="Codul firmei"),
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
@@ -41,6 +42,8 @@ async def get_invoices(
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
server_id = getattr(request.state, 'server_id', None)
filter_params = InvoiceFilter(
company=company,
partner_type=partner_type,
@@ -55,7 +58,7 @@ async def get_invoices(
page_size=page_size
)
result = await InvoiceService.get_invoices(filter_params, current_user.username)
result = await InvoiceService.get_invoices(filter_params, current_user.username, server_id=server_id)
return result
except ValueError as e:
@@ -65,6 +68,7 @@ async def get_invoices(
@router.get("/summary", response_model=InvoiceSummary)
async def get_invoices_summary(
request: Request,
company: str = Query(description="Codul firmei"),
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
current_user: CurrentUser = Depends(get_current_user)
@@ -75,7 +79,9 @@ async def get_invoices_summary(
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await InvoiceService.get_invoice_summary(company, partner_type, current_user.username)
server_id = getattr(request.state, 'server_id', None)
result = await InvoiceService.get_invoice_summary(company, partner_type, current_user.username, server_id=server_id)
return result
except Exception as e:
@@ -83,6 +89,7 @@ async def get_invoices_summary(
@router.get("/{invoice_number}")
async def get_invoice_details(
request: Request,
invoice_number: str,
company: str = Query(description="Codul firmei"),
current_user: CurrentUser = Depends(get_current_user)
@@ -92,8 +99,10 @@ async def get_invoice_details(
# Verifică dacă utilizatorul are acces la firma specificată
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
result = await InvoiceService.get_invoice_details(company, invoice_number, current_user.username)
server_id = getattr(request.state, 'server_id', None)
result = await InvoiceService.get_invoice_details(company, invoice_number, current_user.username, server_id=server_id)
return result
except ValueError as e:
@@ -103,6 +112,7 @@ async def get_invoice_details(
@router.get("/export/{format}")
async def export_invoices(
request: Request,
format: str,
company: str = Query(description="Codul firmei"),
partner_type: str = Query("CLIENTI", description="CLIENTI sau FURNIZORI"),
@@ -119,6 +129,8 @@ async def export_invoices(
# Verifică dacă utilizatorul are acces la firma specificată
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
server_id = getattr(request.state, 'server_id', None) # For future use
# Verifică formatul
if format not in ["excel", "pdf", "csv"]:

View File

@@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from typing import Optional, List
from datetime import date
# import sys # Removed - no longer needed
@@ -13,6 +13,7 @@ router = APIRouter()
@router.get("/bank-cash-register", response_model=RegisterListResponse)
async def get_bank_cash_register(
request: Request,
company: str = Query(description="Codul firmei"),
register_type: Optional[str] = Query(None, description="Tipul registrului: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA sau None pentru toate"),
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna contabilă (1-12)"),
@@ -37,6 +38,8 @@ async def get_bank_cash_register(
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
server_id = getattr(request.state, 'server_id', None)
# Validează register_type dacă e specificat
valid_types = ['BANCA_LEI', 'BANCA_VALUTA', 'CASA_LEI', 'CASA_VALUTA']
if register_type and register_type not in valid_types:
@@ -74,7 +77,7 @@ async def get_bank_cash_register(
page_size=page_size
)
result = await TreasuryService.get_bank_cash_register(filter_params, current_user.username)
result = await TreasuryService.get_bank_cash_register(filter_params, current_user.username, server_id=server_id)
return result
except ValueError as e:
@@ -85,6 +88,7 @@ async def get_bank_cash_register(
@router.get("/bank-cash-accounts", response_model=List[str])
async def get_bank_cash_accounts(
request: Request,
company: str = Query(description="Codul firmei"),
register_type: str = Query(description="Tipul registrului: BANCA_LEI, BANCA_VALUTA, CASA_LEI, CASA_VALUTA"),
current_user: CurrentUser = Depends(get_current_user)
@@ -100,6 +104,8 @@ async def get_bank_cash_accounts(
if company not in current_user.companies:
raise HTTPException(status_code=403, detail=f"Nu aveți acces la firma {company}")
server_id = getattr(request.state, 'server_id', None)
# Validează register_type
valid_types = ['BANCA_LEI', 'BANCA_VALUTA', 'CASA_LEI', 'CASA_VALUTA']
if register_type not in valid_types:
@@ -108,7 +114,7 @@ async def get_bank_cash_accounts(
detail=f"Tip registru invalid. Valori acceptate: {', '.join(valid_types)}"
)
result = await TreasuryService.get_bank_cash_accounts(int(company), register_type)
result = await TreasuryService.get_bank_cash_accounts(int(company), register_type, server_id=server_id)
return result
except ValueError as e:

View File

@@ -2,7 +2,7 @@
API Router for Trial Balance (Balanță de Verificare)
Refactored to use service layer with caching
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from typing import Optional
from datetime import date
# import sys # Removed - no longer needed
@@ -20,6 +20,7 @@ router = APIRouter()
@router.get("/", response_model=TrialBalanceResponse)
async def get_trial_balance(
request: Request,
company: str = Query(description="Codul firmei (ID)"),
luna: Optional[int] = Query(None, ge=1, le=12, description="Luna (1-12), default: luna curentă"),
an: Optional[int] = Query(None, ge=2000, le=2100, description="An, default: anul curent"),
@@ -48,6 +49,8 @@ async def get_trial_balance(
detail=f"Nu aveți acces la firma {company}"
)
server_id = getattr(request.state, 'server_id', None)
# Setează valorile implicite pentru lună și an (luna și anul curent)
current_date = date.today()
if luna is None:
@@ -69,7 +72,8 @@ async def get_trial_balance(
sort_order=sort_order,
page=page,
page_size=page_size,
username=current_user.username
username=current_user.username,
server_id=server_id
)
return TrialBalanceResponse(

View File

@@ -3,6 +3,7 @@ Calendar service for fetching available accounting periods
"""
# import sys # Removed - no longer needed
import os
from typing import Optional
from shared.database.oracle_pool import oracle_pool
from ..models.calendar import CalendarPeriod, CalendarPeriodsResponse
@@ -22,10 +23,10 @@ class CalendarService:
]
@staticmethod
@cached(cache_type='schema', key_params=['company_id'])
async def _get_schema(company_id: int) -> str:
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""Get schema for company (CACHED 24h)"""
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
cursor.execute("""
SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme
@@ -35,19 +36,19 @@ class CalendarService:
return result[0] if result else None
@staticmethod
@cached(cache_type='calendar_periods', key_params=['company_id'])
async def get_available_periods(company_id: int) -> CalendarPeriodsResponse:
@cached(cache_type='calendar_periods', key_params=['company_id', 'server_id'])
async def get_available_periods(company_id: int, server_id: Optional[str] = None) -> CalendarPeriodsResponse:
"""
Get all available accounting periods for a company (CACHED 1h)
Returns periods ordered by year DESC, month DESC with Romanian month names.
"""
schema = await CalendarService._get_schema(company_id)
schema = await CalendarService._get_schema(company_id, server_id)
if not schema:
logger.warning(f"Schema not found for company {company_id}")
return CalendarPeriodsResponse(periods=[], current_period=None, total_count=0)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
cursor.execute(f"""
SELECT anul, luna

View File

@@ -44,15 +44,15 @@ class DashboardService:
return cte_sql, params
@staticmethod
@cached(cache_type='schema', key_params=['company_id'])
async def _get_schema(company_id: int) -> str:
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""
Obține schema pentru company_id (CACHED PERMANENT)
CRITICAL: Acest query este cel mai frecvent - executat la FIECARE request API.
Cache permanent reduce queries cu 99.99%.
"""
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
schema_query = """
SELECT schema
@@ -68,8 +68,8 @@ class DashboardService:
return schema_result[0]
@staticmethod
@cached(cache_type='dashboard_summary', key_params=['company', 'username', 'luna', 'an'])
async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> DashboardSummary:
@cached(cache_type='dashboard_summary', key_params=['company', 'username', 'luna', 'an', 'server_id'])
async def get_complete_summary(company: str, username: str, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> DashboardSummary:
"""
Obține toate datele pentru dashboard într-un singur apel (CACHED 30 min)
Execută 2 query-uri separate: facturi și trezorerie
@@ -80,14 +80,15 @@ class DashboardService:
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
request: Request object pentru cache metadata
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
company_id = int(company)
schema = await DashboardService._get_schema(company_id)
schema = await DashboardService._get_schema(company_id, server_id)
# Construiește CTE pentru perioada curentă
period_cte, period_params = DashboardService._build_period_cte(schema, luna, an)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Query 1: Statistici facturi cu breakdown pe perioade - FIXED ORA-00937
@@ -565,8 +566,8 @@ class DashboardService:
)
@staticmethod
@cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an'])
async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
@cached(cache_type='dashboard_trends', key_params=['company_id', 'period', 'luna', 'an', 'server_id'])
async def get_trends(company_id: int, period: str = "12m", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""Get comprehensive trend analysis data for all dashboard indicators (CACHED 30 min)
Args:
@@ -575,11 +576,12 @@ class DashboardService:
luna: Luna contabilă (1-12), opțional - dacă nu e specificată, folosește MAX
an: Anul contabil, opțional - dacă nu e specificat, folosește MAX
request: Request object pentru cache metadata
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
try:
schema = await DashboardService._get_schema(company_id)
schema = await DashboardService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Determine current period from params or database
@@ -962,7 +964,7 @@ class DashboardService:
raise
@staticmethod
async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = ""):
async def get_detailed_data(company: str, data_type: str, luna: Optional[int] = None, an: Optional[int] = None, page: int = 1, page_size: int = 25, search: str = "", server_id: Optional[str] = None):
"""
Obține date detaliate pentru tabelele din dashboard
Fixed to use existing vireg_parteneri view instead of missing tables
@@ -975,9 +977,10 @@ class DashboardService:
page: Pagina curentă
page_size: Mărimea paginii
search: Termen de căutare
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
logger.info(f"get_detailed_data called: company={company}, data_type={data_type}, luna={luna}, an={an}, page={page}")
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
try:
# Get schema for company
@@ -1168,14 +1171,15 @@ class DashboardService:
return {"error": f"Database error: {str(e)}", "data": [], "total": 0}
@staticmethod
async def get_performance_data(company_id: int, period: str = "7d") -> Dict[str, Any]:
async def get_performance_data(company_id: int, period: str = "7d", server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Calculează performanța încasări/plăți pentru perioada selectată
Args:
company_id: ID-ul companiei
period: Perioada ("7d", "1m", "3m", "6m", "ytd", "12m")
server_id: ID-ul serverului Oracle (pentru multi-server)
Returns:
{
labels: List[str] - etichete pentru perioadele de timp
@@ -1190,7 +1194,7 @@ class DashboardService:
}
"""
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Get schema
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
@@ -1262,14 +1266,15 @@ class DashboardService:
raise
@staticmethod
async def get_cashflow_forecast(company_id: int, period: str = "7d") -> Dict[str, Any]:
async def get_cashflow_forecast(company_id: int, period: str = "7d", server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Calculează previziunea cash flow bazată pe scadențe
Args:
company_id: ID-ul companiei
period: Perioada ("7d", "1m", "3m", "6m")
server_id: ID-ul serverului Oracle (pentru multi-server)
Returns:
{
periods: List[str] - perioadele de timp
@@ -1282,7 +1287,7 @@ class DashboardService:
}
"""
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Get schema
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
@@ -1347,8 +1352,8 @@ class DashboardService:
raise
@staticmethod
@cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an'])
async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
@cached(cache_type='maturity_analysis', key_params=['company_id', 'period', 'luna', 'an', 'server_id'])
async def get_maturity_analysis(company_id: int, period: str = "7d", luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Analizează scadențele clienți vs furnizori cu date reale din Oracle (CACHED 30 min)
@@ -1357,6 +1362,7 @@ class DashboardService:
period: Perioada ("7d", "1m", "3m", "6m", "12m", "over12m")
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
server_id: ID-ul serverului Oracle (pentru multi-server)
Returns:
{
@@ -1367,7 +1373,7 @@ class DashboardService:
}
"""
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Get schema
schema_query = "SELECT schema FROM CONTAFIN_ORACLE.v_nom_firme WHERE id_firma = :company_id"
@@ -1546,8 +1552,8 @@ class DashboardService:
raise
@staticmethod
@cached(cache_type='monthly_flows', key_params=['company', 'luna', 'an'])
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
@cached(cache_type='monthly_flows', key_params=['company', 'luna', 'an', 'server_id'])
async def get_monthly_flows(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Obține fluxurile lunare de intrare și ieșire pentru luna curentă (CACHED 30 min)
@@ -1556,9 +1562,10 @@ class DashboardService:
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
request: Request object pentru cache metadata
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema
company_id = company
@@ -1640,8 +1647,8 @@ class DashboardService:
raise
@staticmethod
@cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an'])
async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
@cached(cache_type='treasury_breakdown', key_params=['company', 'luna', 'an', 'server_id'])
async def get_treasury_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Obține breakdown-ul trezoreriei pe casă și bancă (CACHED 30 min)
@@ -1649,9 +1656,10 @@ class DashboardService:
company: ID-ul firmei
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema
company_id = company
@@ -1745,8 +1753,8 @@ class DashboardService:
raise
@staticmethod
@cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an'])
async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None) -> Dict[str, Any]:
@cached(cache_type='net_balance_breakdown', key_params=['company', 'luna', 'an', 'server_id'])
async def get_net_balance_breakdown(company: int, luna: Optional[int] = None, an: Optional[int] = None, request: Optional[Request] = None, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Obține breakdown-ul balanței nete pe clienți și furnizori cu detaliere pe perioade (CACHED 30 min)
@@ -1754,9 +1762,10 @@ class DashboardService:
company: ID-ul firmei
luna: Luna contabilă (1-12), opțional
an: Anul contabil, opțional
server_id: ID-ul serverului Oracle (pentru multi-server)
"""
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema
company_id = company
@@ -1938,12 +1947,13 @@ class DashboardService:
raise
@staticmethod
async def get_current_period(company: int) -> Dict[str, Any]:
async def get_current_period(company: int, server_id: Optional[str] = None) -> Dict[str, Any]:
"""
Obține perioada curentă (an și lună) din calendarul Oracle
Args:
company: ID-ul companiei
server_id: ID-ul serverului Oracle (pentru multi-server)
Returns:
{
@@ -1953,7 +1963,7 @@ class DashboardService:
}
"""
try:
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema
company_id = company

View File

@@ -278,14 +278,14 @@ class FinancialIndicatorsService:
"""
@staticmethod
@cached(cache_type='schema', key_params=['company_id'])
async def _get_schema(company_id: int) -> str:
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""
Obține schema pentru company_id (CACHED PERMANENT)
Schema este stocată permanent în cache deoarece nu se schimbă.
"""
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
schema_query = """
SELECT schema
@@ -319,11 +319,12 @@ class FinancialIndicatorsService:
return f"SUM(CASE WHEN ({conditions}) THEN NVL({column}, 0) ELSE 0 END)"
@staticmethod
@cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_balance_sheet', key_params=['company_id', 'luna', 'an', 'server_id'])
async def get_balance_sheet_aggregates(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> BalanceSheetAggregates:
"""
Obține soldurile agregate din balanța de verificare pentru calculul
@@ -343,9 +344,9 @@ class FinancialIndicatorsService:
Raises:
ValueError: Dacă schema nu este găsită pentru firma specificată
"""
schema = await FinancialIndicatorsService._get_schema(company_id)
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Construim query-ul cu CASE pentru fiecare categorie
# Soldurile din VBAL: SOLDDEB (sold debitor), SOLDCRED (sold creditor)
@@ -546,11 +547,12 @@ class FinancialIndicatorsService:
return aggregates
@staticmethod
@cached(cache_type='fin_achizitii', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_achizitii', key_params=['company_id', 'luna', 'an', 'server_id'])
async def get_achizitii_ytd(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> Decimal:
"""
Calculează totalul achizițiilor YTD din Registrul Jurnal (ACT).
@@ -575,9 +577,9 @@ class FinancialIndicatorsService:
Returns:
Total achiziții YTD fără TVA (Decimal)
"""
schema = await FinancialIndicatorsService._get_schema(company_id)
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
query = f"""
SELECT
@@ -611,11 +613,12 @@ class FinancialIndicatorsService:
return achizitii_total
@staticmethod
@cached(cache_type='fin_cashflow_vbal', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_cashflow_vbal', key_params=['company_id', 'luna', 'an', 'server_id'])
async def get_cashflow_from_vbal(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> dict:
"""
Calculează datele de Cash Flow direct din VBAL (balanța de verificare).
@@ -642,9 +645,9 @@ class FinancialIndicatorsService:
- incasari_ytd: Încasări YTD (4111+461 TOTCRED)
- plati_ytd: Plăți YTD (401+404+462 TOTDEB)
"""
schema = await FinancialIndicatorsService._get_schema(company_id)
schema = await FinancialIndicatorsService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
query = f"""
SELECT
@@ -737,7 +740,8 @@ class FinancialIndicatorsService:
async def calculate_liquidity_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> LiquidityIndicators:
"""
Calculează indicatorii de lichiditate pentru evaluarea capacității
@@ -763,7 +767,7 @@ class FinancialIndicatorsService:
"""
# Obținem agregatele din balanță
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
@@ -906,11 +910,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_efficiency', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_efficiency', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_efficiency_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> EfficiencyIndicators:
"""
Calculează indicatorii de eficiență pentru evaluarea vitezei de conversie
@@ -944,7 +949,8 @@ class FinancialIndicatorsService:
company=str(company_id),
username="system", # System call for indicators
luna=luna,
an=an
an=an,
server_id=server_id
)
# Ensure summary is a DashboardSummary model (cache may return dict)
if isinstance(summary, dict):
@@ -953,7 +959,8 @@ class FinancialIndicatorsService:
# Obținem datele din trends (facturări/încasări/achiziții/plăți lunare)
trends = await DashboardService.get_trends(
company_id=company_id,
period='12m' # Ultimele 12 luni pentru media lunară
period='12m', # Ultimele 12 luni pentru media lunară
server_id=server_id
)
# Extragem soldurile din summary
@@ -1162,11 +1169,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_risk', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_risk', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_risk_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> RiskIndicators:
"""
Calculează indicatorii de risc și aging pentru evaluarea sănătății
@@ -1205,7 +1213,8 @@ class FinancialIndicatorsService:
company=str(company_id),
username="system", # System call for indicators
luna=luna,
an=an
an=an,
server_id=server_id
)
# Ensure summary is a DashboardSummary model (cache may return dict)
if isinstance(summary, dict):
@@ -1384,11 +1393,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_cashflow', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_cashflow', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_cashflow_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> CashFlowIndicators:
"""
Calculează indicatorii de cash flow pentru evaluarea generării și
@@ -1421,10 +1431,10 @@ class FinancialIndicatorsService:
# Obținem datele de cash flow din VBAL (sursa preferată)
# VBAL oferă date directe: RULCRED/RULDEB pentru lunar, TOTCRED/TOTDEB pentru YTD
cf_data_curent = await FinancialIndicatorsService.get_cashflow_from_vbal(
company_id, luna, an
company_id, luna, an, server_id
)
cf_data_anterior = await FinancialIndicatorsService.get_cashflow_from_vbal(
company_id, luna, an - 1
company_id, luna, an - 1, server_id
)
# Obținem datele din summary pentru datorii restante
@@ -1432,7 +1442,8 @@ class FinancialIndicatorsService:
company=str(company_id),
username="system",
luna=luna,
an=an
an=an,
server_id=server_id
)
# Ensure summary is a DashboardSummary model (cache may return dict)
if isinstance(summary, dict):
@@ -1609,11 +1620,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_dynamics', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_dynamics', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_dynamics_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> DynamicsIndicators:
"""
Calculează indicatorii de dinamică pentru evaluarea evoluției afacerii.
@@ -1653,10 +1665,10 @@ class FinancialIndicatorsService:
# Obținem agregatele pentru anul curent și anul anterior
# Cifra de Afaceri (70x) din VBAL - FĂRĂ TVA
aggregates_curent = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
aggregates_anterior = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an - 1
company_id, luna, an - 1, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
@@ -1674,10 +1686,10 @@ class FinancialIndicatorsService:
# Include: 3x=4x (stocuri) + 6x=4x (servicii, consumabile)
# Exclude: discount/rabat (40x=667/609)
achizitii_curent = await FinancialIndicatorsService.get_achizitii_ytd(
company_id, luna, an
company_id, luna, an, server_id
)
achizitii_anterior = await FinancialIndicatorsService.get_achizitii_ytd(
company_id, luna, an - 1
company_id, luna, an - 1, server_id
)
total_achizitii_curent = float(achizitii_curent)
total_achizitii_anterior = float(achizitii_anterior)
@@ -1843,11 +1855,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_altman', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_altman', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_altman_zscore(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> AltmanZScore:
"""
Calculează Altman Z-Score pentru evaluarea riscului de faliment.
@@ -1880,7 +1893,7 @@ class FinancialIndicatorsService:
"""
# Obținem agregatele din balanță
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
@@ -2088,11 +2101,12 @@ class FinancialIndicatorsService:
return result
@staticmethod
@cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an'])
@cached(cache_type='fin_profitability', key_params=['company_id', 'luna', 'an', 'server_id'])
async def calculate_profitability_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> ProfitabilityIndicators:
"""
Calculează indicatorii de profitabilitate pentru evaluarea randamentului afacerii.
@@ -2120,7 +2134,7 @@ class FinancialIndicatorsService:
"""
# Obținem agregatele din balanță (include venituri, cheltuieli, active, capital)
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
@@ -2356,7 +2370,8 @@ class FinancialIndicatorsService:
async def calculate_solvability_indicators(
company_id: int,
luna: int,
an: int
an: int,
server_id: Optional[str] = None
) -> SolvabilityIndicators:
"""
Calculează indicatorii de solvabilitate pentru evaluarea capacității
@@ -2384,7 +2399,7 @@ class FinancialIndicatorsService:
"""
# Obținem agregatele din balanță
aggregates = await FinancialIndicatorsService.get_balance_sheet_aggregates(
company_id, luna, an
company_id, luna, an, server_id
)
# Ensure aggregates is a BalanceSheetAggregates model (cache may return dict)
if isinstance(aggregates, dict):
@@ -2555,12 +2570,13 @@ class FinancialIndicatorsService:
return periods
@staticmethod
@cached(cache_type='financial_indicators_historical', ttl=3600, key_params=['company_id', 'months', 'luna', 'an'])
@cached(cache_type='financial_indicators_historical', ttl=3600, key_params=['company_id', 'months', 'luna', 'an', 'server_id'])
async def get_historical_indicators(
company_id: int,
months: int = 12,
luna: Optional[int] = None,
an: Optional[int] = None
an: Optional[int] = None,
server_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Calculează indicatorii financiari pentru ultimele `months` luni
@@ -2672,7 +2688,7 @@ class FinancialIndicatorsService:
try:
# Lichiditate
lichiditate = await FinancialIndicatorsService.calculate_liquidity_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure lichiditate is a model (cache may return dict)
if isinstance(lichiditate, dict):
@@ -2690,7 +2706,7 @@ class FinancialIndicatorsService:
# Eficiență
eficienta = await FinancialIndicatorsService.calculate_efficiency_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure eficienta is a model (cache may return dict)
if isinstance(eficienta, dict):
@@ -2706,7 +2722,7 @@ class FinancialIndicatorsService:
# Risc
risc = await FinancialIndicatorsService.calculate_risk_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure risc is a model (cache may return dict)
if isinstance(risc, dict):
@@ -2725,7 +2741,7 @@ class FinancialIndicatorsService:
# Cash Flow
cash_flow = await FinancialIndicatorsService.calculate_cashflow_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure cash_flow is a model (cache may return dict)
if isinstance(cash_flow, dict):
@@ -2742,7 +2758,7 @@ class FinancialIndicatorsService:
# Dinamica
dinamica = await FinancialIndicatorsService.calculate_dynamics_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure dinamica is a model (cache may return dict)
if isinstance(dinamica, dict):
@@ -2758,7 +2774,7 @@ class FinancialIndicatorsService:
# Altman Z-Score
altman = await FinancialIndicatorsService.calculate_altman_zscore(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure altman is a model (cache may return dict)
if isinstance(altman, dict):
@@ -2772,7 +2788,7 @@ class FinancialIndicatorsService:
# Profitabilitate
profitabilitate = await FinancialIndicatorsService.calculate_profitability_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure profitabilitate is a model (cache may return dict)
if isinstance(profitabilitate, dict):
@@ -2795,7 +2811,7 @@ class FinancialIndicatorsService:
# Solvabilitate
solvabilitate = await FinancialIndicatorsService.calculate_solvability_indicators(
company_id, period_luna, period_an
company_id, period_luna, period_an, server_id
)
# Ensure solvabilitate is a model (cache may return dict)
if isinstance(solvabilitate, dict):
@@ -2829,13 +2845,14 @@ class FinancialIndicatorsService:
return historical_data
@staticmethod
@cached(cache_type='financial_indicators_sparklines', key_params=['company_id', 'luna', 'an', 'months'])
@cached(cache_type='financial_indicators_sparklines', key_params=['company_id', 'luna', 'an', 'months', 'server_id'])
async def get_indicators_with_sparklines(
company_id: int,
luna: int,
an: int,
months: int = 12,
request: Optional[Request] = None
request: Optional[Request] = None,
server_id: Optional[str] = None
) -> FinancialIndicatorsResponse:
"""
Calculează toți indicatorii financiari și adaugă datele de sparkline
@@ -2858,32 +2875,32 @@ class FinancialIndicatorsService:
# Obținem datele istorice și indicatorii curenți în paralel
historical_task = FinancialIndicatorsService.get_historical_indicators(
company_id, months, luna, an
company_id, months, luna, an, server_id
)
lichiditate_task = FinancialIndicatorsService.calculate_liquidity_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
eficienta_task = FinancialIndicatorsService.calculate_efficiency_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
risc_task = FinancialIndicatorsService.calculate_risk_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
cash_flow_task = FinancialIndicatorsService.calculate_cashflow_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
dinamica_task = FinancialIndicatorsService.calculate_dynamics_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
altman_task = FinancialIndicatorsService.calculate_altman_zscore(
company_id, luna, an
company_id, luna, an, server_id
)
profitabilitate_task = FinancialIndicatorsService.calculate_profitability_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
solvabilitate_task = FinancialIndicatorsService.calculate_solvability_indicators(
company_id, luna, an
company_id, luna, an, server_id
)
(

View File

@@ -5,7 +5,7 @@ Service pentru logica facturi - Portează query-urile din aplicația Flask
import os
from shared.database.oracle_pool import oracle_pool
from typing import List, Tuple
from typing import List, Tuple, Optional
from ..models.invoice import Invoice, InvoiceFilter, InvoiceListResponse, InvoiceSummary
from ..cache.decorators import cached
from decimal import Decimal
@@ -17,10 +17,10 @@ class InvoiceService:
"""Service pentru gestionarea facturilor"""
@staticmethod
@cached(cache_type='schema', key_params=['company_id'])
async def _get_schema(company_id: int) -> str:
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""Obține schema pentru company_id (CACHED PERMANENT)"""
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
schema_query = """
SELECT schema
@@ -36,15 +36,15 @@ class InvoiceService:
return schema_result[0]
@staticmethod
@cached(cache_type='invoices', key_params=['filter_params', 'username'])
async def get_invoices(filter_params: InvoiceFilter, username: str) -> InvoiceListResponse:
@cached(cache_type='invoices', key_params=['filter_params', 'username', 'server_id'])
async def get_invoices(filter_params: InvoiceFilter, username: str, server_id: Optional[str] = None) -> InvoiceListResponse:
"""
Obține lista de facturi - Query simplu pentru afișare în tabel (CACHED 10 min)
"""
company_id = int(filter_params.company)
schema = await InvoiceService._get_schema(company_id)
schema = await InvoiceService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Determină conturile în funcție de partner_type
@@ -240,11 +240,11 @@ class InvoiceService:
)
@staticmethod
async def get_invoice_details(company: str, invoice_number: str, username: str) -> Invoice:
async def get_invoice_details(company: str, invoice_number: str, username: str, server_id: Optional[str] = None) -> Invoice:
"""
Obține detaliile unei facturi specifice
"""
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Obține schema din v_nom_firme bazat pe id_firma
company_id = int(company)

View File

@@ -1,12 +1,12 @@
# import sys # Removed - no longer needed
import os
from typing import Optional, List, Tuple, Any
import oracledb
from shared.database.oracle_pool import oracle_pool
from ..models.treasury import BankCashRegister, RegisterFilter, RegisterListResponse, AccountingPeriod
from ..cache.decorators import cached
from decimal import Decimal
from typing import Optional, List, Tuple, Any
import logging
logger = logging.getLogger(__name__)
@@ -15,10 +15,10 @@ class TreasuryService:
"""Service pentru trezorerie - registru casă și bancă"""
@staticmethod
@cached(cache_type='schema', key_params=['company_id'])
async def _get_schema(company_id: int) -> str:
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""Obține schema pentru company_id (CACHED PERMANENT)"""
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
schema_query = """
SELECT schema
@@ -99,8 +99,8 @@ class TreasuryService:
return " UNION ALL ".join(queries)
@staticmethod
@cached(cache_type='treasury', key_params=['filter_params', 'username'])
async def get_bank_cash_register(filter_params: RegisterFilter, username: str) -> RegisterListResponse:
@cached(cache_type='treasury', key_params=['filter_params', 'username', 'server_id'])
async def get_bank_cash_register(filter_params: RegisterFilter, username: str, server_id: Optional[str] = None) -> RegisterListResponse:
"""
Obține registrul de casă și bancă din vbancasa views (CACHED 10 min)
@@ -114,9 +114,9 @@ class TreasuryService:
Toate în aceeași tranzacție!
"""
company_id = int(filter_params.company)
schema = await TreasuryService._get_schema(company_id)
schema = await TreasuryService._get_schema(company_id, server_id)
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Construiește query-ul pentru tipul de registru selectat
@@ -350,14 +350,14 @@ class TreasuryService:
)
@staticmethod
@cached(cache_type='treasury', key_params=['company_id', 'register_type'])
async def get_bank_cash_accounts(company_id: int, register_type: str) -> List[str]:
@cached(cache_type='treasury', key_params=['company_id', 'register_type', 'server_id'])
async def get_bank_cash_accounts(company_id: int, register_type: str, server_id: Optional[str] = None) -> List[str]:
"""
Obține lista distinctă de conturi bancă/casă (bancasa) pentru dropdown.
Cached pentru performanță.
IMPORTANT: Trebuie să setăm contextul PACK_SESIUNE înainte de a accesa vbancasa views!
"""
schema = await TreasuryService._get_schema(company_id)
schema = await TreasuryService._get_schema(company_id, server_id)
# Map register_type to view
view_map = {
@@ -372,7 +372,7 @@ class TreasuryService:
view_name = view_map[register_type]
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# PL/SQL block to set session context and get accounts
plsql_block = f"""

View File

@@ -4,9 +4,9 @@ Refactored to use caching system for optimal performance
"""
# import sys # Removed - no longer needed
import os
from typing import Dict, Any, Optional
from shared.database.oracle_pool import oracle_pool
from typing import Dict, Any
from ..models.trial_balance import (
TrialBalanceItem,
TrialBalanceFilters,
@@ -25,14 +25,14 @@ class TrialBalanceService:
"""Service pentru gestionarea balanței de verificare cu cache"""
@staticmethod
@cached(cache_type='schema', key_params=['company_id'])
async def _get_schema(company_id: int) -> str:
@cached(cache_type='schema', key_params=['company_id', 'server_id'])
async def _get_schema(company_id: int, server_id: Optional[str] = None) -> str:
"""
Obține schema pentru company_id (CACHED 24h)
This is cached permanently because company schemas rarely change.
"""
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
schema_query = """
SELECT schema
@@ -50,7 +50,7 @@ class TrialBalanceService:
@staticmethod
@cached(cache_type='trial_balance', key_params=['company_id', 'luna', 'an', 'cont_filter',
'denumire_filter', 'sort_by', 'sort_order',
'page', 'page_size', 'username'])
'page', 'page_size', 'username', 'server_id'])
async def get_trial_balance(
company_id: int,
luna: int,
@@ -61,7 +61,8 @@ class TrialBalanceService:
sort_order: str,
page: int,
page_size: int,
username: str
username: str,
server_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Obține balanța de verificare sintetică (CACHED 10 min)
@@ -80,12 +81,13 @@ class TrialBalanceService:
page: Pagina
page_size: Mărimea paginii
username: Username pentru cache tracking
server_id: Optional Oracle server identifier for multi-server support
Returns:
Dictionary cu items, pagination, filters_applied
"""
# Get schema (cached separately)
schema = await TrialBalanceService._get_schema(company_id)
schema = await TrialBalanceService._get_schema(company_id, server_id)
# Validate sort_order
if sort_order.lower() not in ['asc', 'desc']:
@@ -97,7 +99,7 @@ class TrialBalanceService:
if sort_by.upper() not in valid_sort_columns:
sort_by = 'CONT'
async with oracle_pool.get_connection() as connection:
async with oracle_pool.get_connection(server_id) as connection:
with connection.cursor() as cursor:
# Build base query for VBAL VIEW
base_query = f"""

View File

@@ -17,6 +17,9 @@ logger = logging.getLogger(__name__)
DB_DIR = Path(__file__).parent.parent.parent / "data"
DB_PATH = DB_DIR / "telegram_bot.db"
# SQLite busy timeout in milliseconds (wait for locks instead of failing immediately)
SQLITE_BUSY_TIMEOUT_MS = 5000
async def get_db_connection() -> aiosqlite.Connection:
"""
@@ -41,6 +44,10 @@ async def init_database() -> None:
logger.info(f"Database directory: {DB_DIR}")
async with aiosqlite.connect(DB_PATH) as db:
# Enable WAL mode for better concurrent access
await db.execute("PRAGMA journal_mode=WAL")
# Set busy timeout to wait for locks instead of failing immediately
await db.execute(f"PRAGMA busy_timeout={SQLITE_BUSY_TIMEOUT_MS}")
# Enable foreign keys
await db.execute("PRAGMA foreign_keys = ON")

View File

@@ -43,6 +43,7 @@ async def create_or_update_user(
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
INSERT INTO telegram_users (
@@ -77,6 +78,7 @@ async def get_user(telegram_user_id: int) -> Optional[Dict[str, Any]]:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_users
@@ -115,6 +117,7 @@ async def link_user_to_oracle(
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE telegram_users
@@ -163,6 +166,7 @@ async def update_user_tokens(
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE telegram_users
@@ -193,6 +197,7 @@ async def update_user_last_active(telegram_user_id: int) -> bool:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE telegram_users
@@ -220,6 +225,7 @@ async def is_user_linked(telegram_user_id: int) -> bool:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT oracle_username FROM telegram_users
@@ -246,6 +252,7 @@ async def is_user_authenticated(telegram_user_id: int) -> bool:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT oracle_username, jwt_token, token_expires_at
@@ -299,6 +306,7 @@ async def create_auth_code(
expires_at = datetime.now() + timedelta(minutes=expires_in_minutes)
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
INSERT INTO telegram_auth_codes (
@@ -328,6 +336,7 @@ async def get_auth_code(code: str) -> Optional[Dict[str, Any]]:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_auth_codes
@@ -356,6 +365,7 @@ async def verify_and_use_auth_code(code: str) -> Optional[Dict[str, Any]]:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
# Check if code exists, is not used, and not expired
cursor = await db.execute("""
@@ -399,6 +409,7 @@ async def get_pending_codes_for_user(telegram_user_id: int) -> List[Dict[str, An
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_auth_codes
@@ -431,6 +442,7 @@ async def get_pending_email_code(
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT code, email, oracle_username, expires_at, failed_attempts
@@ -476,6 +488,7 @@ async def create_email_auth_code(
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
INSERT INTO email_auth_codes
@@ -500,6 +513,7 @@ async def get_email_auth_code(code: str) -> Optional[Dict]:
"""Get email auth code details"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT code, email, oracle_username, telegram_user_id,
@@ -534,6 +548,7 @@ async def increment_failed_attempts(code: str) -> bool:
"""Increment failed validation attempts for code"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE email_auth_codes
@@ -553,6 +568,7 @@ async def mark_email_code_used(code: str) -> bool:
"""Mark email code as used"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE email_auth_codes
@@ -574,6 +590,7 @@ async def delete_user_email_codes(telegram_user_id: int) -> int:
"""Delete all email codes for user (cleanup)"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
DELETE FROM email_auth_codes
@@ -616,6 +633,7 @@ async def create_session(
expires_at = datetime.now() + timedelta(hours=expires_in_hours)
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
INSERT INTO telegram_sessions (
@@ -645,6 +663,7 @@ async def get_session(session_id: str) -> Optional[Dict[str, Any]]:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_sessions
@@ -674,6 +693,7 @@ async def get_user_active_session(telegram_user_id: int) -> Optional[Dict[str, A
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
SELECT * FROM telegram_sessions
@@ -709,6 +729,7 @@ async def update_session_state(
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
UPDATE telegram_sessions
@@ -738,6 +759,7 @@ async def delete_session(session_id: str) -> bool:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
await db.execute("""
DELETE FROM telegram_sessions
@@ -765,6 +787,7 @@ async def delete_user_sessions(telegram_user_id: int) -> bool:
"""
try:
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("PRAGMA busy_timeout=5000")
db.row_factory = aiosqlite.Row
cursor = await db.execute("""
DELETE FROM telegram_sessions