diff --git a/crud.py b/crud.py index e92e47e..8abcbbb 100644 --- a/crud.py +++ b/crud.py @@ -1,3 +1,4 @@ +import json from datetime import datetime, timedelta, timezone from typing import Optional @@ -6,6 +7,30 @@ from lnbits.helpers import urlsafe_short_hash from .models import CreateEvent, Event, Ticket, TicketExtra + +def _parse_ticket_row(row) -> dict: + """ + Parse a database row into a dict suitable for Ticket model creation. + Handles: + - Empty string to None conversion for name/email + - JSON string to dict conversion for extra field + """ + ticket_data = dict(row) + + # Convert empty strings back to None for the model + if ticket_data.get("name") == "": + ticket_data["name"] = None + if ticket_data.get("email") == "": + ticket_data["email"] = None + + # Parse extra field from JSON string if needed + # (db.insert() serializes to JSON, but manual fetchone/fetchall returns string) + extra = ticket_data.get("extra") + if isinstance(extra, str): + ticket_data["extra"] = json.loads(extra) + + return ticket_data + db = Database("ext_events") @@ -20,7 +45,12 @@ async def create_ticket( ) -> Ticket: now = datetime.now(timezone.utc) - # Handle database constraints: if user_id is provided, use empty strings for name/email + # TODO: Check if this empty string workaround is still needed. + # This converts None to empty strings for database storage because: + # 1. Database may have NOT NULL constraints on name/email columns + # 2. When user_id is provided, name/email are not used (mutually exclusive) + # 3. The get_ticket() functions convert empty strings back to None when reading + # Consider using nullable columns instead of this empty string pattern. if user_id: db_name = "" db_email = "" @@ -28,7 +58,28 @@ async def create_ticket( db_name = name or "" db_email = email or "" - ticket = Ticket( + # Create ticket with database-compatible values for insertion + # Using db.insert() ensures proper serialization of the extra field (TicketExtra) + # across all database backends (SQLite, PostgreSQL, CockroachDB) + db_ticket = Ticket( + id=payment_hash, + wallet=wallet, + event=event, + name=db_name, + email=db_email, + user_id=user_id, + registered=False, + paid=False, + reg_timestamp=now, + time=now, + extra=TicketExtra(**extra) if extra else TicketExtra(), + ) + + await db.insert("events.ticket", db_ticket) + + # Return ticket with original name/email values (not empty strings) + # This maintains consistency with how get_ticket() converts empty strings back to None + return Ticket( id=payment_hash, wallet=wallet, event=event, @@ -42,20 +93,6 @@ async def create_ticket( extra=TicketExtra(**extra) if extra else TicketExtra(), ) - # Create a dict for database insertion with proper handling of constraints - ticket_dict = ticket.dict() - ticket_dict["name"] = db_name - ticket_dict["email"] = db_email - - await db.execute( - """ - INSERT INTO events.ticket (id, wallet, event, name, email, user_id, registered, paid, time, reg_timestamp, extra) - VALUES (:id, :wallet, :event, :name, :email, :user_id, :registered, :paid, :time, :reg_timestamp, :extra) - """, - ticket_dict - ) - return ticket - async def update_ticket(ticket: Ticket) -> Ticket: # Create a new Ticket object with corrected values for database constraints @@ -82,14 +119,7 @@ async def get_ticket(payment_hash: str) -> Optional[Ticket]: if not row: return None - # Convert empty strings back to None for the model - ticket_data = dict(row) - if ticket_data.get("name") == "": - ticket_data["name"] = None - if ticket_data.get("email") == "": - ticket_data["email"] = None - - return Ticket(**ticket_data) + return Ticket(**_parse_ticket_row(row)) async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: @@ -98,17 +128,7 @@ async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids]) rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})") - tickets = [] - for row in rows: - # Convert empty strings back to None for the model - ticket_data = dict(row) - if ticket_data.get("name") == "": - ticket_data["name"] = None - if ticket_data.get("email") == "": - ticket_data["email"] = None - tickets.append(Ticket(**ticket_data)) - - return tickets + return [Ticket(**_parse_ticket_row(row)) for row in rows] async def get_tickets_by_user_id(user_id: str) -> list[Ticket]: @@ -118,17 +138,7 @@ async def get_tickets_by_user_id(user_id: str) -> list[Ticket]: {"user_id": user_id} ) - tickets = [] - for row in rows: - # Convert empty strings back to None for the model - ticket_data = dict(row) - if ticket_data.get("name") == "": - ticket_data["name"] = None - if ticket_data.get("email") == "": - ticket_data["email"] = None - tickets.append(Ticket(**ticket_data)) - - return tickets + return [Ticket(**_parse_ticket_row(row)) for row in rows] async def delete_ticket(payment_hash: str) -> None: @@ -200,14 +210,4 @@ async def get_event_tickets(event_id: str) -> list[Ticket]: {"event": event_id}, ) - tickets = [] - for row in rows: - # Convert empty strings back to None for the model - ticket_data = dict(row) - if ticket_data.get("name") == "": - ticket_data["name"] = None - if ticket_data.get("email") == "": - ticket_data["email"] = None - tickets.append(Ticket(**ticket_data)) - - return tickets + return [Ticket(**_parse_ticket_row(row)) for row in rows]