import os from contextlib import contextmanager from typing import Any, Dict, List, Optional, Sequence import psycopg2 from psycopg2.extras import RealDictCursor DATABASE_URL = os.environ.get("NEON_DATABASE_URL") if not DATABASE_URL: raise RuntimeError( "Brak zmiennej NEON_DATABASE_URL. Ustaw sekret w Hugging Face lub " "ustaw zmienną środowiskową lokalnie." ) @contextmanager def db_conn(): conn = psycopg2.connect(DATABASE_URL, cursor_factory=RealDictCursor) try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def fetch_one(query: str, params: Sequence[Any]) -> Optional[Dict[str, Any]]: with db_conn() as conn, conn.cursor() as cur: cur.execute(query, params) return cur.fetchone() def fetch_all(query: str, params: Sequence[Any] = ()) -> List[Dict[str, Any]]: with db_conn() as conn, conn.cursor() as cur: cur.execute(query, params) return cur.fetchall() def execute(query: str, params: Sequence[Any]) -> None: with db_conn() as conn, conn.cursor() as cur: cur.execute(query, params) def create_account(login: str, email: str, password_hash: str) -> int: with db_conn() as conn, conn.cursor() as cur: cur.execute( """ INSERT INTO accounts (login, password_hash) VALUES (%s, %s) RETURNING id """, (login, password_hash), ) account_id = cur.fetchone()["id"] cur.execute( """ INSERT INTO business_profiles (account_id, company_name, owner_name, address_line, postal_code, city, tax_id, bank_account) VALUES (%s, '', '', '', '', '', '', '') """, (account_id,), ) return account_id def update_business(account_id: int, data: Dict[str, str]) -> None: execute( """ UPDATE business_profiles SET company_name = %s, owner_name = %s, address_line = %s, postal_code = %s, city = %s, tax_id = %s, bank_account = %s WHERE account_id = %s """, ( data["company_name"], data["owner_name"], data["address_line"], data["postal_code"], data["city"], data["tax_id"], data["bank_account"], account_id, ), ) def fetch_business_logo(account_id: int) -> Optional[Dict[str, Optional[str]]]: row = fetch_one( """ SELECT logo_mime_type, logo_data_base64 FROM business_profiles WHERE account_id = %s """, (account_id,), ) if not row: return None mime_type = row.get("logo_mime_type") data_base64 = row.get("logo_data_base64") if not mime_type or not data_base64: return None return {"mime_type": mime_type, "data": data_base64} def update_business_logo(account_id: int, mime: Optional[str], data_base64: Optional[str]) -> None: execute( """ UPDATE business_profiles SET logo_mime_type = %s, logo_data_base64 = %s WHERE account_id = %s """, (mime, data_base64, account_id), ) def upsert_client(account_id: int, payload: Dict[str, str]) -> int: row = fetch_one( """ SELECT id FROM clients WHERE account_id = %s AND tax_id = %s """, (account_id, payload["tax_id"]), ) if row: client_id = row["id"] execute( """ UPDATE clients SET name = %s, address_line = %s, postal_code = %s, city = %s, phone = %s WHERE id = %s """, ( payload["name"], payload["address_line"], payload["postal_code"], payload["city"], payload.get("phone"), client_id, ), ) return client_id with db_conn() as conn, conn.cursor() as cur: cur.execute( """ INSERT INTO clients (account_id, name, address_line, postal_code, city, tax_id, phone) VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id """, ( account_id, payload["name"], payload["address_line"], payload["postal_code"], payload["city"], payload["tax_id"], payload.get("phone"), ), ) return cur.fetchone()["id"] def search_clients(account_id: int, term: str, limit: int = 10) -> List[Dict[str, Any]]: query = (term or "").strip().lower() like = f"%{query}%" return fetch_all( """ SELECT name, tax_id, address_line, postal_code, city, phone FROM clients WHERE account_id = %s AND ( %s = '' OR LOWER(COALESCE(name, '')) LIKE %s OR LOWER(COALESCE(tax_id, '')) LIKE %s ) ORDER BY LOWER(COALESCE(name, tax_id, '')) ASC LIMIT %s """, (account_id, query, like, like, limit), ) def insert_invoice(account_id: int, client_id: int, invoice: Dict[str, Any]) -> int: with db_conn() as conn, conn.cursor() as cur: cur.execute( """ INSERT INTO invoices (account_id, client_id, invoice_number, issued_at, sale_date, payment_term_days, exemption_note, total_net, total_vat, total_gross) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, ( account_id, client_id, invoice["invoice_id"], invoice["issued_at"], invoice["sale_date"], invoice.get("payment_term", 14), invoice.get("exemption_note"), invoice["totals"]["net"], invoice["totals"]["vat"], invoice["totals"]["gross"], ), ) invoice_id = cur.fetchone()["id"] cur.executemany( """ INSERT INTO invoice_items (invoice_id, line_no, name, quantity, unit, vat_code, vat_label, unit_price_net, unit_price_gross, net_total, vat_amount, gross_total) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """, [ ( invoice_id, idx + 1, item["name"], item["quantity"], item.get("unit"), item.get("vat_code"), item.get("vat_label"), item["unit_price_net"], item["unit_price_gross"], item["net_total"], item["vat_amount"], item["gross_total"], ) for idx, item in enumerate(invoice["items"]) ], ) cur.executemany( """ INSERT INTO invoice_vat_summary (invoice_id, vat_label, net_total, vat_total, gross_total) VALUES (%s, %s, %s, %s, %s) """, [ ( invoice_id, row["vat_label"], row["net_total"], row["vat_total"], row["gross_total"], ) for row in invoice["summary"] ], ) return invoice_id