- Added a new column `subscriptions_locked` to the `customers` table to manage subscription access. - Implemented a script to create new modules from a template, including updates to various files (module.json, README.md, router.py, views.py, and migration SQL). - Developed a script to import BMC Office subscriptions from an Excel file into the database, including error handling and statistics reporting. - Created a script to lookup and update missing CVR numbers using the CVR.dk API. - Implemented a script to relink Hub customers to e-conomic customer numbers based on name matching. - Developed scripts to sync CVR numbers from Simply-CRM and vTiger to the local customers database.
223 lines
6.3 KiB
Python
223 lines
6.3 KiB
Python
"""
|
|
Database Module
|
|
PostgreSQL connection and helpers using psycopg2
|
|
"""
|
|
|
|
import psycopg2
|
|
from psycopg2.extras import RealDictCursor
|
|
from psycopg2.pool import SimpleConnectionPool
|
|
from typing import Optional
|
|
import logging
|
|
|
|
from app.core.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Connection pool
|
|
connection_pool: Optional[SimpleConnectionPool] = None
|
|
|
|
|
|
def init_db():
|
|
"""Initialize database connection pool"""
|
|
global connection_pool
|
|
|
|
try:
|
|
connection_pool = SimpleConnectionPool(
|
|
minconn=1,
|
|
maxconn=10,
|
|
dsn=settings.DATABASE_URL
|
|
)
|
|
logger.info("✅ Database connection pool initialized")
|
|
except Exception as e:
|
|
logger.error(f"❌ Failed to initialize database: {e}")
|
|
raise
|
|
|
|
|
|
def get_db_connection():
|
|
"""Get a connection from the pool"""
|
|
if connection_pool:
|
|
return connection_pool.getconn()
|
|
raise Exception("Database pool not initialized")
|
|
|
|
|
|
def release_db_connection(conn):
|
|
"""Return a connection to the pool"""
|
|
if connection_pool:
|
|
connection_pool.putconn(conn)
|
|
|
|
|
|
def get_db():
|
|
"""Context manager for database connections"""
|
|
conn = get_db_connection()
|
|
try:
|
|
yield conn
|
|
finally:
|
|
release_db_connection(conn)
|
|
|
|
|
|
def execute_query(query: str, params: Optional[tuple] = None, fetchone: bool = False):
|
|
"""
|
|
Execute a SQL query and return results
|
|
|
|
Args:
|
|
query: SQL query string
|
|
params: Query parameters tuple
|
|
fetchone: If True, return single row dict, otherwise list of dicts
|
|
|
|
Returns:
|
|
Single dict if fetchone=True, otherwise list of dicts
|
|
"""
|
|
conn = get_db_connection()
|
|
try:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
|
|
cursor.execute(query, params or ())
|
|
|
|
# Check if this is a write operation (INSERT, UPDATE, DELETE)
|
|
query_upper = query.strip().upper()
|
|
is_write = any(query_upper.startswith(cmd) for cmd in ['INSERT', 'UPDATE', 'DELETE'])
|
|
|
|
if fetchone:
|
|
row = cursor.fetchone()
|
|
if is_write:
|
|
conn.commit()
|
|
return dict(row) if row else None
|
|
else:
|
|
rows = cursor.fetchall()
|
|
if is_write:
|
|
conn.commit()
|
|
return [dict(row) for row in rows]
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logger.error(f"Query error: {e}")
|
|
raise
|
|
finally:
|
|
release_db_connection(conn)
|
|
|
|
|
|
def execute_insert(query: str, params: tuple = ()) -> Optional[int]:
|
|
"""
|
|
Execute an INSERT query and return last row id
|
|
|
|
Args:
|
|
query: SQL INSERT query (will add RETURNING id if not present)
|
|
params: Query parameters tuple
|
|
|
|
Returns:
|
|
Last inserted row ID or None
|
|
"""
|
|
conn = get_db_connection()
|
|
try:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
|
|
# PostgreSQL requires RETURNING clause
|
|
if "RETURNING" not in query.upper():
|
|
query = query.rstrip(";") + " RETURNING id"
|
|
cursor.execute(query, params)
|
|
result = cursor.fetchone()
|
|
conn.commit()
|
|
|
|
# If result exists, return the first column value (typically ID)
|
|
if result:
|
|
# If it's a dict, get first value
|
|
if isinstance(result, dict):
|
|
return list(result.values())[0]
|
|
# If it's a tuple/list, get first element
|
|
return result[0]
|
|
return None
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logger.error(f"Insert error: {e}")
|
|
raise
|
|
finally:
|
|
release_db_connection(conn)
|
|
|
|
|
|
def execute_update(query: str, params: tuple = ()) -> int:
|
|
"""
|
|
Execute an UPDATE/DELETE query and return affected rows
|
|
|
|
Args:
|
|
query: SQL UPDATE/DELETE query
|
|
params: Query parameters tuple
|
|
|
|
Returns:
|
|
Number of affected rows
|
|
"""
|
|
conn = get_db_connection()
|
|
try:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
|
|
cursor.execute(query, params)
|
|
rowcount = cursor.rowcount
|
|
conn.commit()
|
|
return rowcount
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logger.error(f"Update error: {e}")
|
|
raise
|
|
finally:
|
|
release_db_connection(conn)
|
|
|
|
|
|
def execute_module_migration(module_name: str, migration_sql: str) -> bool:
|
|
"""
|
|
Kør en migration for et specifikt modul
|
|
|
|
Args:
|
|
module_name: Navn på modulet
|
|
migration_sql: SQL migration kode
|
|
|
|
Returns:
|
|
True hvis success, False ved fejl
|
|
"""
|
|
conn = get_db_connection()
|
|
try:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
|
|
# Sikr at module_migrations tabel eksisterer
|
|
cursor.execute("""
|
|
CREATE TABLE IF NOT EXISTS module_migrations (
|
|
id SERIAL PRIMARY KEY,
|
|
module_name VARCHAR(100) NOT NULL,
|
|
migration_name VARCHAR(255) NOT NULL,
|
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
success BOOLEAN DEFAULT TRUE,
|
|
error_message TEXT,
|
|
UNIQUE(module_name, migration_name)
|
|
)
|
|
""")
|
|
|
|
# Kør migration
|
|
cursor.execute(migration_sql)
|
|
conn.commit()
|
|
|
|
logger.info(f"✅ Migration for {module_name} success")
|
|
return True
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logger.error(f"❌ Migration failed for {module_name}: {e}")
|
|
return False
|
|
finally:
|
|
release_db_connection(conn)
|
|
|
|
|
|
def check_module_table_exists(table_name: str) -> bool:
|
|
"""
|
|
Check om en modul tabel eksisterer
|
|
|
|
Args:
|
|
table_name: Tabel navn (fx "my_module_customers")
|
|
|
|
Returns:
|
|
True hvis tabellen eksisterer
|
|
"""
|
|
query = """
|
|
SELECT EXISTS (
|
|
SELECT FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name = %s
|
|
)
|
|
"""
|
|
result = execute_query(query, (table_name,), fetchone=True)
|
|
if result and isinstance(result, dict):
|
|
return result.get('exists', False)
|
|
return False
|