rename lan
This commit was merged in pull request #22.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, ROUND_DOWN
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class InsufficientFundsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AccountingService:
|
||||
def __init__(self, db_service: DatabaseService) -> None:
|
||||
self._db_service = db_service
|
||||
self._update_hooks: set[Callable] = set()
|
||||
|
||||
def add_update_hook(self, update_hook: Callable) -> None:
|
||||
""" Adds a function to this service, which is called whenever the account balance changes """
|
||||
self._update_hooks.add(update_hook)
|
||||
|
||||
async def add_balance(self, user_id: int, balance_to_add: Decimal, reference: str) -> Decimal:
|
||||
await self._db_service.add_transaction(Transaction(
|
||||
user_id=user_id,
|
||||
value=balance_to_add,
|
||||
is_debit=False,
|
||||
reference=reference,
|
||||
transaction_date=datetime.now()
|
||||
))
|
||||
logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user with ID {user_id}")
|
||||
for update_hook in self._update_hooks:
|
||||
await update_hook()
|
||||
return await self.get_balance(user_id)
|
||||
|
||||
async def remove_balance(self, user_id: int, balance_to_remove: Decimal, reference: str) -> Decimal:
|
||||
current_balance = await self.get_balance(user_id)
|
||||
if (current_balance - balance_to_remove) < 0:
|
||||
raise InsufficientFundsError
|
||||
await self._db_service.add_transaction(Transaction(
|
||||
user_id=user_id,
|
||||
value=balance_to_remove,
|
||||
is_debit=True,
|
||||
reference=reference,
|
||||
transaction_date=datetime.now()
|
||||
))
|
||||
logger.debug(
|
||||
f"Removed balance of {self.make_euro_string_from_decimal(balance_to_remove)} to user with ID {user_id}")
|
||||
for update_hook in self._update_hooks:
|
||||
await update_hook()
|
||||
return await self.get_balance(user_id)
|
||||
|
||||
async def get_balance(self, user_id: int) -> Decimal:
|
||||
balance_buffer = Decimal("0")
|
||||
for transaction in await self._db_service.get_all_transactions_for_user(user_id):
|
||||
if transaction.is_debit:
|
||||
balance_buffer -= transaction.value
|
||||
else:
|
||||
balance_buffer += transaction.value
|
||||
return balance_buffer
|
||||
|
||||
async def get_transaction_history(self, user_id: int) -> list[Transaction]:
|
||||
return await self._db_service.get_all_transactions_for_user(user_id)
|
||||
|
||||
@staticmethod
|
||||
def make_euro_string_from_decimal(euros: Optional[Decimal]) -> str:
|
||||
"""
|
||||
Internally, all money values are euros as decimal. Only when showing them to the user we generate a string.
|
||||
"""
|
||||
if euros is None:
|
||||
return "0.00 €"
|
||||
rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN))
|
||||
return f"{rounded_decimal} €"
|
||||
@@ -0,0 +1,147 @@
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.services.UserService import UserService
|
||||
from src.ezgg_lan_manager.services.ReceiptPrintingService import ReceiptPrintingService
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder, CateringOrderStatus, CateringMenuItemsWithAmount
|
||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class CateringErrorType(Enum):
|
||||
INCLUDES_DISABLED_ITEM = 0
|
||||
INSUFFICIENT_FUNDS = 1
|
||||
GENERIC = 99
|
||||
|
||||
|
||||
class CateringError(Exception):
|
||||
def __init__(self, message: str, error_type: CateringErrorType = CateringErrorType.GENERIC) -> None:
|
||||
self.message = message
|
||||
self.error_type = error_type
|
||||
|
||||
|
||||
class CateringService:
|
||||
def __init__(self, db_service: DatabaseService, accounting_service: AccountingService, user_service: UserService, receipt_printing_service: ReceiptPrintingService) -> None:
|
||||
self._db_service = db_service
|
||||
self._accounting_service = accounting_service
|
||||
self._user_service = user_service
|
||||
self._receipt_printing_service = receipt_printing_service
|
||||
self.cached_cart: dict[int, list[CateringMenuItem]] = {}
|
||||
|
||||
# ORDERS
|
||||
|
||||
async def place_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int,
|
||||
is_delivery: bool = True) -> CateringOrder:
|
||||
for menu_item in menu_items:
|
||||
if menu_item.is_disabled:
|
||||
raise CateringError("Order includes disabled items", CateringErrorType.INCLUDES_DISABLED_ITEM)
|
||||
|
||||
user = await self._user_service.get_user(user_id)
|
||||
if not user:
|
||||
raise CateringError("User does not exist")
|
||||
|
||||
total_price = sum([item.price * quantity for item, quantity in menu_items.items()], Decimal(0))
|
||||
if await self._accounting_service.get_balance(user_id) < total_price:
|
||||
raise CateringError("Insufficient funds", CateringErrorType.INSUFFICIENT_FUNDS)
|
||||
|
||||
order = await self._db_service.add_new_order(menu_items, user_id, is_delivery)
|
||||
if order:
|
||||
await self._accounting_service.remove_balance(user_id, total_price, f"CATERING - {order.order_id}")
|
||||
logger.info(
|
||||
f"User '{order.customer.user_name}' (ID:{order.customer.user_id}) ordered from catering for {self._accounting_service.make_euro_string_from_decimal(total_price)}")
|
||||
await self._receipt_printing_service.print_order(user, order)
|
||||
# await self.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order
|
||||
return order
|
||||
|
||||
async def update_order_status(self, order_id: int, new_status: CateringOrderStatus) -> bool:
|
||||
if new_status == CateringOrderStatus.CANCELED:
|
||||
# Cancelled orders need to be refunded
|
||||
raise CateringError("Orders cannot be canceled this way, use CateringService.cancel_order")
|
||||
return await self._db_service.change_order_status(order_id, new_status)
|
||||
|
||||
async def get_orders(self) -> list[CateringOrder]:
|
||||
return await self._db_service.get_orders()
|
||||
|
||||
async def get_orders_for_user(self, user_id: int) -> list[CateringOrder]:
|
||||
return await self._db_service.get_orders(user_id=user_id)
|
||||
|
||||
async def get_orders_by_status(self, status: CateringOrderStatus) -> list[CateringOrder]:
|
||||
return await self._db_service.get_orders(status=status)
|
||||
|
||||
async def cancel_order(self, order: CateringOrder) -> bool:
|
||||
change_result = await self._db_service.change_order_status(order.order_id, CateringOrderStatus.CANCELED)
|
||||
if change_result:
|
||||
await self._accounting_service.add_balance(order.customer.user_id, order.price,
|
||||
f"CATERING REFUND - {order.order_id}")
|
||||
return True
|
||||
return False
|
||||
|
||||
# MENU ITEMS
|
||||
|
||||
async def get_menu(self, category: Optional[CateringMenuItemCategory] = None) -> list[CateringMenuItem]:
|
||||
items = await self._db_service.get_menu_items()
|
||||
if not category:
|
||||
return items
|
||||
return list(filter(lambda item: item.category == category, items))
|
||||
|
||||
async def get_menu_item_by_id(self, menu_item_id: int) -> CateringMenuItem:
|
||||
item = await self._db_service.get_menu_item(menu_item_id)
|
||||
if not item:
|
||||
raise CateringError("Menu item not found")
|
||||
return item
|
||||
|
||||
async def add_menu_item(self, name: str, info: str, price: Decimal, category: CateringMenuItemCategory,
|
||||
is_disabled: bool = False) -> CateringMenuItem:
|
||||
if new_item := await self._db_service.add_menu_item(name, info, price, category, is_disabled):
|
||||
return new_item
|
||||
raise CateringError(f"Could not add item '{name}' to the menu.")
|
||||
|
||||
async def remove_menu_item(self, menu_item_id: int) -> bool:
|
||||
return await self._db_service.delete_menu_item(menu_item_id)
|
||||
|
||||
async def change_menu_item(self, updated_item: CateringMenuItem) -> bool:
|
||||
return await self._db_service.update_menu_item(updated_item)
|
||||
|
||||
async def disable_menu_item(self, menu_item_id: int) -> bool:
|
||||
try:
|
||||
item = await self.get_menu_item_by_id(menu_item_id)
|
||||
except CateringError:
|
||||
return False
|
||||
item.is_disabled = True
|
||||
return await self._db_service.update_menu_item(item)
|
||||
|
||||
async def enable_menu_item(self, menu_item_id: int) -> bool:
|
||||
try:
|
||||
item = await self.get_menu_item_by_id(menu_item_id)
|
||||
except CateringError:
|
||||
return False
|
||||
item.is_disabled = False
|
||||
return await self._db_service.update_menu_item(item)
|
||||
|
||||
async def disable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool:
|
||||
items = await self.get_menu(category=category)
|
||||
return all([self.disable_menu_item(item.item_id) for item in items])
|
||||
|
||||
async def enable_menu_items_by_category(self, category: CateringMenuItemCategory) -> bool:
|
||||
items = await self.get_menu(category=category)
|
||||
return all([self.enable_menu_item(item.item_id) for item in items])
|
||||
|
||||
# CART
|
||||
|
||||
def save_cart(self, user_id: Optional[int], cart: list[CateringMenuItem]) -> None:
|
||||
if user_id:
|
||||
self.cached_cart[user_id] = cart
|
||||
|
||||
def get_cart(self, user_id: Optional[int]) -> list[CateringMenuItem]:
|
||||
if user_id is None:
|
||||
return []
|
||||
try:
|
||||
return self.cached_cart[user_id]
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import tomllib
|
||||
|
||||
from from_root import from_root
|
||||
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, \
|
||||
SeatingConfiguration, TicketInfo, ReceiptPrintingConfiguration
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class ConfigurationService:
|
||||
def __init__(self, config_file_path: Path) -> None:
|
||||
try:
|
||||
with open(from_root("VERSION"), "r") as version_file:
|
||||
self._version = version_file.read().strip()
|
||||
except FileNotFoundError:
|
||||
logger.warning("Could not find VERSION file, defaulting to '0.0.0'")
|
||||
self._version = "0.0.0"
|
||||
|
||||
try:
|
||||
with open(config_file_path, "rb") as config_file:
|
||||
self._config = tomllib.load(config_file)
|
||||
except FileNotFoundError:
|
||||
logger.fatal(f"Could not find config file at \"{config_file_path}\", exiting...")
|
||||
exit(1)
|
||||
|
||||
def get_database_configuration(self) -> DatabaseConfiguration:
|
||||
try:
|
||||
database_configuration = self._config["database"]
|
||||
return DatabaseConfiguration(
|
||||
db_user=database_configuration["db_user"],
|
||||
db_password=database_configuration["db_password"],
|
||||
db_host=database_configuration["db_host"],
|
||||
db_port=database_configuration["db_port"],
|
||||
db_name=database_configuration["db_name"]
|
||||
)
|
||||
except KeyError:
|
||||
logger.fatal("Error loading DatabaseConfiguration, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
def get_mailing_service_configuration(self) -> MailingServiceConfiguration:
|
||||
try:
|
||||
mailing_configuration = self._config["mailing"]
|
||||
return MailingServiceConfiguration(
|
||||
smtp_server=mailing_configuration["smtp_server"],
|
||||
smtp_port=mailing_configuration["smtp_port"],
|
||||
sender=mailing_configuration["sender"],
|
||||
username=mailing_configuration["username"],
|
||||
password=mailing_configuration["password"]
|
||||
)
|
||||
except KeyError:
|
||||
logger.fatal("Error loading MailingServiceConfiguration, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
def get_lan_info(self) -> LanInfo:
|
||||
try:
|
||||
lan_info = self._config["lan"]
|
||||
return LanInfo(
|
||||
name=lan_info["name"],
|
||||
iteration=lan_info["iteration"],
|
||||
date_from=datetime.strptime(lan_info["date_from"], "%Y-%m-%d %H:%M:%S"),
|
||||
date_till=datetime.strptime(lan_info["date_till"], "%Y-%m-%d %H:%M:%S"),
|
||||
organizer_mail=lan_info["organizer_mail"]
|
||||
)
|
||||
except KeyError:
|
||||
logger.fatal("Error loading LAN Info, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
def get_ticket_info(self) -> tuple[TicketInfo, ...]:
|
||||
try:
|
||||
return tuple([TicketInfo(
|
||||
category=value,
|
||||
total_tickets=self._config["tickets"][value]["total_tickets"],
|
||||
price=Decimal(self._config["tickets"][value]["price"]),
|
||||
description=self._config["tickets"][value]["description"],
|
||||
additional_info=self._config["tickets"][value]["additional_info"],
|
||||
is_default=self._config["tickets"][value]["is_default"]
|
||||
) for value in self._config["tickets"]])
|
||||
except KeyError as e:
|
||||
logger.debug(e)
|
||||
logger.fatal("Error loading seating configuration, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
def get_receipt_printing_configuration(self) -> ReceiptPrintingConfiguration:
|
||||
try:
|
||||
receipt_printing_configuration = self._config["receipt_printing"]
|
||||
return ReceiptPrintingConfiguration(
|
||||
host=receipt_printing_configuration["host"],
|
||||
port=receipt_printing_configuration["port"],
|
||||
order_print_endpoint=receipt_printing_configuration["order_print_endpoint"],
|
||||
password=receipt_printing_configuration["password"]
|
||||
)
|
||||
except KeyError:
|
||||
logger.fatal("Error loading Receipt Printing Configuration, exiting...")
|
||||
sys.exit(1)
|
||||
|
||||
@property
|
||||
def APP_VERSION(self) -> str:
|
||||
return self._version
|
||||
|
||||
@property
|
||||
def DEV_MODE_ACTIVE(self) -> bool:
|
||||
return self._config["misc"]["dev_mode_active"]
|
||||
@@ -0,0 +1,789 @@
|
||||
import logging
|
||||
|
||||
from datetime import date, datetime
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
import aiomysql
|
||||
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder
|
||||
from src.ezgg_lan_manager.types.CateringMenuItem import CateringMenuItem, CateringMenuItemCategory
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringMenuItemsWithAmount, CateringOrderStatus
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import DatabaseConfiguration
|
||||
from src.ezgg_lan_manager.types.News import News
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
from src.ezgg_lan_manager.types.Transaction import Transaction
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class DuplicationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoDatabaseConnectionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DatabaseService:
|
||||
MAX_CONNECTION_RETRIES = 5
|
||||
|
||||
def __init__(self, database_config: DatabaseConfiguration) -> None:
|
||||
self._database_config = database_config
|
||||
self._connection_pool: Optional[aiomysql.Pool] = None
|
||||
|
||||
async def is_healthy(self) -> bool:
|
||||
try:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor() as _:
|
||||
return True
|
||||
except aiomysql.OperationalError:
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to acquire a connection: {e}")
|
||||
return False
|
||||
|
||||
async def init_db_pool(self) -> bool:
|
||||
logger.info(
|
||||
f"Connecting to database '{self._database_config.db_name}' on "
|
||||
f"{self._database_config.db_user}@{self._database_config.db_host}:{self._database_config.db_port}"
|
||||
)
|
||||
try:
|
||||
self._connection_pool = await aiomysql.create_pool(
|
||||
host=self._database_config.db_host,
|
||||
port=self._database_config.db_port,
|
||||
user=self._database_config.db_user,
|
||||
password=self._database_config.db_password,
|
||||
db=self._database_config.db_name,
|
||||
minsize=1,
|
||||
maxsize=40
|
||||
)
|
||||
except aiomysql.OperationalError:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _map_db_result_to_user(data: tuple) -> User:
|
||||
return User(
|
||||
user_id=data[0],
|
||||
user_name=data[1],
|
||||
user_mail=data[2],
|
||||
user_password=data[3],
|
||||
user_first_name=data[4],
|
||||
user_last_name=data[5],
|
||||
user_birth_day=data[6],
|
||||
is_active=bool(data[7]),
|
||||
is_team_member=bool(data[8]),
|
||||
is_admin=bool(data[9]),
|
||||
created_at=data[10],
|
||||
last_updated_at=data[11]
|
||||
)
|
||||
|
||||
async def get_user_by_name(self, user_name: str) -> Optional[User]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE user_name=%s", (user_name,))
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
return
|
||||
return self._map_db_result_to_user(result)
|
||||
|
||||
async def get_user_by_id(self, user_id: int) -> Optional[User]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE user_id=%s", (user_id,))
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
return
|
||||
return self._map_db_result_to_user(result)
|
||||
|
||||
async def get_user_by_mail(self, user_mail: str) -> Optional[User]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
await cursor.execute("SELECT * FROM users WHERE user_mail=%s", (user_mail.lower(),))
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
return
|
||||
return self._map_db_result_to_user(result)
|
||||
|
||||
async def create_user(self, user_name: str, user_mail: str, password_hash: str) -> User:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO users (user_name, user_mail, user_password) "
|
||||
"VALUES (%s, %s, %s)", (user_name, user_mail.lower(), password_hash)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.create_user(user_name, user_mail, password_hash)
|
||||
except aiomysql.IntegrityError as e:
|
||||
logger.warning(f"Aborted duplication entry: {e}")
|
||||
raise DuplicationError
|
||||
|
||||
return await self.get_user_by_name(user_name)
|
||||
|
||||
async def update_user(self, user: User) -> User:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE users SET user_name=%s, user_mail=%s, user_password=%s, user_first_name=%s, "
|
||||
"user_last_name=%s, user_birth_date=%s, is_active=%s, is_team_member=%s, is_admin=%s "
|
||||
"WHERE (user_id=%s)",
|
||||
(user.user_name, user.user_mail.lower(), user.user_password,
|
||||
user.user_first_name, user.user_last_name, user.user_birth_day,
|
||||
user.is_active, user.is_team_member, user.is_admin,
|
||||
user.user_id)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.update_user(user)
|
||||
except aiomysql.IntegrityError as e:
|
||||
logger.warning(f"Aborted duplication entry: {e}")
|
||||
raise DuplicationError
|
||||
return user
|
||||
|
||||
async def add_transaction(self, transaction: Transaction) -> Optional[Transaction]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO transactions (user_id, value, is_debit, transaction_date, transaction_reference) "
|
||||
"VALUES (%s, %s, %s, %s, %s)",
|
||||
(transaction.user_id, transaction.value, transaction.is_debit, transaction.transaction_date,
|
||||
transaction.reference)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_transaction(transaction)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding Transaction: {e}")
|
||||
return
|
||||
|
||||
return transaction
|
||||
|
||||
async def get_all_transactions_for_user(self, user_id: int) -> list[Transaction]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
transactions = []
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM transactions WHERE user_id=%s", (user_id,))
|
||||
await conn.commit()
|
||||
result = await cursor.fetchall()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_all_transactions_for_user(user_id)
|
||||
except aiomysql.Error as e:
|
||||
logger.error(f"Error getting all transactions for user: {e}")
|
||||
return []
|
||||
|
||||
for transaction_raw in result:
|
||||
transactions.append(Transaction(
|
||||
user_id=user_id,
|
||||
value=Decimal(transaction_raw[2]),
|
||||
is_debit=bool(transaction_raw[3]),
|
||||
transaction_date=transaction_raw[4],
|
||||
reference=transaction_raw[5]
|
||||
))
|
||||
return transactions
|
||||
|
||||
async def add_news(self, news: News) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO news (news_content, news_title, news_subtitle, news_author, news_date) "
|
||||
"VALUES (%s, %s, %s, %s, %s)",
|
||||
(news.content, news.title, news.subtitle, news.author.user_id, news.news_date)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_news(news)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding Transaction: {e}")
|
||||
|
||||
async def get_news(self, dt_start: date, dt_end: date) -> list[News]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute(
|
||||
"SELECT * FROM news INNER JOIN users ON news.news_author = users.user_id WHERE news_date"
|
||||
" BETWEEN %s AND %s;",
|
||||
(dt_start, dt_end))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_news(dt_start, dt_end)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching news: {e}")
|
||||
return []
|
||||
|
||||
for news_raw in await cursor.fetchall():
|
||||
user = self._map_db_result_to_user(news_raw[6:])
|
||||
results.append(News(
|
||||
news_id=news_raw[0],
|
||||
title=news_raw[2],
|
||||
subtitle=news_raw[3],
|
||||
author=user,
|
||||
content=news_raw[1],
|
||||
news_date=news_raw[5]
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def update_news(self, news: News) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"""
|
||||
UPDATE news
|
||||
SET news_content = %s,
|
||||
news_title = %s,
|
||||
news_subtitle = %s,
|
||||
news_author = %s,
|
||||
news_date = %s
|
||||
WHERE news_id = %s
|
||||
""",
|
||||
(news.content, news.title, news.subtitle, news.author.user_id, news.news_date, news.news_id)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.update_news(news)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error updating news: {e}")
|
||||
|
||||
async def remove_news(self, news_id: int) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"DELETE FROM news WHERE news_id = %s",
|
||||
(news_id,)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.remove_news(news_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error removing news with ID {news_id}: {e}")
|
||||
|
||||
async def get_tickets(self) -> list[Ticket]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM tickets INNER JOIN users ON tickets.user = users.user_id;", ())
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_tickets()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching tickets: {e}")
|
||||
return []
|
||||
|
||||
for ticket_raw in await cursor.fetchall():
|
||||
user = self._map_db_result_to_user(ticket_raw[3:])
|
||||
results.append(Ticket(
|
||||
ticket_id=ticket_raw[0],
|
||||
category=ticket_raw[1],
|
||||
purchase_date=ticket_raw[3],
|
||||
owner=user
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def get_ticket_for_user(self, user_id: int) -> Optional[Ticket]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"SELECT * FROM tickets INNER JOIN users ON tickets.user = users.user_id WHERE user_id=%s;",
|
||||
(user_id,))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_ticket_for_user(user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching ticket for user: {e}")
|
||||
return
|
||||
|
||||
result = await cursor.fetchone()
|
||||
if not result:
|
||||
return
|
||||
|
||||
user = self._map_db_result_to_user(result[3:])
|
||||
return Ticket(
|
||||
ticket_id=result[0],
|
||||
category=result[1],
|
||||
purchase_date=result[3],
|
||||
owner=user
|
||||
)
|
||||
|
||||
async def generate_ticket_for_user(self, user_id: int, category: str) -> Optional[Ticket]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("INSERT INTO tickets (ticket_category, user) VALUES (%s, %s)",
|
||||
(category, user_id))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.generate_ticket_for_user(user_id, category)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error generating ticket for user: {e}")
|
||||
return
|
||||
|
||||
return await self.get_ticket_for_user(user_id)
|
||||
|
||||
async def change_ticket_owner(self, ticket_id: int, new_owner_id: int) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("UPDATE tickets SET user = %s WHERE ticket_id = %s;",
|
||||
(new_owner_id, ticket_id))
|
||||
affected_rows = cursor.rowcount
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.change_ticket_owner(ticket_id, new_owner_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error transferring ticket to user: {e}")
|
||||
return False
|
||||
return affected_rows > 0
|
||||
|
||||
async def delete_ticket(self, ticket_id: int) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("DELETE FROM tickets WHERE ticket_id = %s;", (ticket_id,))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.change_ticket_owner(ticket_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting ticket: {e}")
|
||||
return False
|
||||
return True
|
||||
|
||||
async def generate_fresh_seats_table(self, seats: list[tuple[str, str]]) -> None:
|
||||
""" WARNING: THIS WILL DELETE ALL EXISTING DATA! DO NOT USE ON PRODUCTION DATABASE! """
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("TRUNCATE seats;")
|
||||
for seat in seats:
|
||||
await cursor.execute("INSERT INTO seats (seat_id, seat_category) VALUES (%s, %s);",
|
||||
(seat[0], seat[1]))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.generate_fresh_seats_table(seats)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error generating fresh seats table: {e}")
|
||||
return
|
||||
|
||||
async def get_seating_info(self) -> list[Seat]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute(
|
||||
"SELECT seats.*, users.* FROM seats LEFT JOIN users ON seats.user = users.user_id;")
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_seating_info()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting seats table: {e}")
|
||||
return results
|
||||
|
||||
for seat_raw in await cursor.fetchall():
|
||||
if seat_raw[3] is None: # Empty seat
|
||||
results.append(Seat(seat_raw[0], bool(seat_raw[1]), seat_raw[2], None))
|
||||
else:
|
||||
user = self._map_db_result_to_user(seat_raw[4:])
|
||||
results.append(Seat(seat_raw[0], bool(seat_raw[1]), seat_raw[2], user))
|
||||
|
||||
return results
|
||||
|
||||
async def seat_user(self, seat_id: str, user_id: int) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("UPDATE seats SET user = %s WHERE seat_id = %s;", (user_id, seat_id))
|
||||
affected_rows = cursor.rowcount
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.seat_user(seat_id, user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error seating user: {e}")
|
||||
return False
|
||||
return affected_rows > 0
|
||||
|
||||
async def get_menu_items(self) -> list[CateringMenuItem]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM catering_menu_items;")
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_menu_items()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching menu items: {e}")
|
||||
return results
|
||||
|
||||
for menu_item_raw in await cursor.fetchall():
|
||||
results.append(CateringMenuItem(
|
||||
item_id=menu_item_raw[0],
|
||||
name=menu_item_raw[1],
|
||||
additional_info=menu_item_raw[2],
|
||||
price=Decimal(menu_item_raw[3]),
|
||||
category=CateringMenuItemCategory(menu_item_raw[4]),
|
||||
is_disabled=bool(menu_item_raw[5])
|
||||
))
|
||||
|
||||
return results
|
||||
|
||||
async def get_menu_item(self, menu_item_id) -> Optional[CateringMenuItem]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM catering_menu_items WHERE catering_menu_item_id = %s;",
|
||||
(menu_item_id,))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_menu_item(menu_item_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching menu items: {e}")
|
||||
return
|
||||
|
||||
raw_data = await cursor.fetchone()
|
||||
if raw_data is None:
|
||||
return
|
||||
return CateringMenuItem(
|
||||
item_id=raw_data[0],
|
||||
name=raw_data[1],
|
||||
additional_info=raw_data[2],
|
||||
price=Decimal(raw_data[3]),
|
||||
category=CateringMenuItemCategory(raw_data[4]),
|
||||
is_disabled=bool(raw_data[5])
|
||||
)
|
||||
|
||||
async def add_menu_item(self, name: str, info: str, price: Decimal, category: CateringMenuItemCategory,
|
||||
is_disabled: bool = False) -> Optional[CateringMenuItem]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO catering_menu_items (name, additional_info, price, category, is_disabled) VALUES "
|
||||
"(%s, %s, %s, %s, %s);",
|
||||
(name, info, price, category.value, is_disabled)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_menu_item(name, info, price, category, is_disabled)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error adding menu item: {e}")
|
||||
return
|
||||
|
||||
return CateringMenuItem(
|
||||
item_id=cursor.lastrowid,
|
||||
name=name,
|
||||
additional_info=info,
|
||||
price=price,
|
||||
category=category,
|
||||
is_disabled=is_disabled
|
||||
)
|
||||
|
||||
async def delete_menu_item(self, menu_item_id: int) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("DELETE FROM catering_menu_items WHERE catering_menu_item_id = %s;",
|
||||
(menu_item_id,))
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.delete_menu_item(menu_item_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting menu item: {e}")
|
||||
return False
|
||||
return cursor.affected_rows > 0
|
||||
|
||||
async def update_menu_item(self, updated_item: CateringMenuItem) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE catering_menu_items SET name = %s, additional_info = %s, price = %s, category = %s, "
|
||||
"is_disabled = %s WHERE catering_menu_item_id = %s;",
|
||||
(updated_item.name, updated_item.additional_info, updated_item.price,
|
||||
updated_item.category.value, updated_item.is_disabled, updated_item.item_id)
|
||||
)
|
||||
affected_rows = cursor.rowcount
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.update_menu_item(updated_item)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error updating menu item: {e}")
|
||||
return False
|
||||
return affected_rows > 0
|
||||
|
||||
async def add_new_order(self, menu_items: CateringMenuItemsWithAmount, user_id: int, is_delivery: bool) -> Optional[CateringOrder]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
now = datetime.now()
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO orders (status, user, is_delivery, order_date) VALUES (%s, %s, %s, %s);",
|
||||
(CateringOrderStatus.RECEIVED.value, user_id, is_delivery, now)
|
||||
)
|
||||
order_id = cursor.lastrowid
|
||||
for menu_item, quantity in menu_items.items():
|
||||
await cursor.execute(
|
||||
"INSERT INTO order_catering_menu_item (order_id, catering_menu_item_id, quantity) VALUES "
|
||||
"(%s, %s, %s);",
|
||||
(order_id, menu_item.item_id, quantity)
|
||||
)
|
||||
await conn.commit()
|
||||
return CateringOrder(
|
||||
order_id=order_id,
|
||||
order_date=now,
|
||||
status=CateringOrderStatus.RECEIVED,
|
||||
items=menu_items,
|
||||
customer=await self.get_user_by_id(user_id),
|
||||
is_delivery=is_delivery
|
||||
)
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.add_new_order(menu_items, user_id, is_delivery)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error placing order: {e}")
|
||||
return
|
||||
|
||||
async def change_order_status(self, order_id: int, status: CateringOrderStatus) -> bool:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"UPDATE orders SET status = %s WHERE order_id = %s;",
|
||||
(status.value, order_id)
|
||||
)
|
||||
affected_rows = cursor.rowcount
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.change_order_status(order_id, status)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error updating menu item: {e}")
|
||||
return False
|
||||
return affected_rows > 0
|
||||
|
||||
async def get_orders(self, user_id: Optional[int] = None, status: Optional[CateringOrderStatus] = None) -> list[CateringOrder]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
fetched_orders = []
|
||||
query = "SELECT * FROM orders LEFT JOIN users ON orders.user = users.user_id"
|
||||
if user_id is not None and status is None:
|
||||
query += f" WHERE user = {user_id};"
|
||||
elif status is not None and user_id is None:
|
||||
query += f" WHERE status = '{status.value}';"
|
||||
elif status is not None and user_id is not None:
|
||||
query += f" WHERE user = {user_id} AND status = '{status.value}';"
|
||||
else:
|
||||
query += ";"
|
||||
try:
|
||||
await cursor.execute(query)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_orders(user_id, status)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting orders: {e}")
|
||||
return fetched_orders
|
||||
|
||||
for raw_order in await cursor.fetchall():
|
||||
fetched_orders.append(
|
||||
CateringOrder(
|
||||
order_id=raw_order[0],
|
||||
status=CateringOrderStatus(raw_order[1]),
|
||||
customer=self._map_db_result_to_user(raw_order[5:]),
|
||||
items=await self.get_menu_items_for_order(raw_order[0]),
|
||||
is_delivery=bool(raw_order[4]),
|
||||
order_date=raw_order[3],
|
||||
)
|
||||
)
|
||||
|
||||
return fetched_orders
|
||||
|
||||
async def get_menu_items_for_order(self, order_id: int) -> CateringMenuItemsWithAmount:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
result = {}
|
||||
try:
|
||||
await cursor.execute(
|
||||
"SELECT * FROM order_catering_menu_item "
|
||||
"LEFT JOIN catering_menu_items ON order_catering_menu_item.catering_menu_item_id = catering_menu_items.catering_menu_item_id "
|
||||
"WHERE order_id = %s;",
|
||||
(order_id,)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_menu_items_for_order(order_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting order items: {e}")
|
||||
return result
|
||||
|
||||
for order_catering_menu_item_raw in await cursor.fetchall():
|
||||
result[CateringMenuItem(
|
||||
item_id=order_catering_menu_item_raw[1],
|
||||
name=order_catering_menu_item_raw[4],
|
||||
additional_info=order_catering_menu_item_raw[5],
|
||||
price=Decimal(order_catering_menu_item_raw[6]),
|
||||
category=CateringMenuItemCategory(order_catering_menu_item_raw[7]),
|
||||
is_disabled=bool(order_catering_menu_item_raw[8])
|
||||
)] = order_catering_menu_item_raw[2]
|
||||
|
||||
return result
|
||||
|
||||
async def set_user_profile_picture(self, user_id: int, picture_data: bytes) -> None:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"INSERT INTO user_profile_picture (user_id, picture) VALUES (%s, %s) ON DUPLICATE KEY UPDATE picture = VALUES(picture)",
|
||||
(user_id, picture_data)
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.set_user_profile_picture(user_id, picture_data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error setting user profile picture: {e}")
|
||||
|
||||
async def get_user_profile_picture(self, user_id: int) -> Optional[bytes]:
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute("SELECT (picture) FROM user_profile_picture WHERE user_id = %s", (user_id,))
|
||||
await conn.commit()
|
||||
r = await cursor.fetchone()
|
||||
if r is None:
|
||||
return
|
||||
return r[0]
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_user_profile_picture(user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error setting user profile picture: {e}")
|
||||
return None
|
||||
|
||||
async def get_all_users(self) -> list[User]:
|
||||
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
results = []
|
||||
try:
|
||||
await cursor.execute("SELECT * FROM users;")
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.get_all_users()
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting all users: {e}")
|
||||
return results
|
||||
|
||||
for user_raw in await cursor.fetchall():
|
||||
results.append(self._map_db_result_to_user(user_raw))
|
||||
|
||||
return results
|
||||
|
||||
async def remove_profile_picture(self, user_id: int):
|
||||
async with self._connection_pool.acquire() as conn:
|
||||
async with conn.cursor(aiomysql.Cursor) as cursor:
|
||||
try:
|
||||
await cursor.execute(
|
||||
"DELETE FROM user_profile_picture WHERE user_id = %s",
|
||||
user_id
|
||||
)
|
||||
await conn.commit()
|
||||
except aiomysql.InterfaceError:
|
||||
pool_init_result = await self.init_db_pool()
|
||||
if not pool_init_result:
|
||||
raise NoDatabaseConnectionError
|
||||
return await self.remove_profile_picture(user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error deleting user profile picture: {e}")
|
||||
@@ -0,0 +1,25 @@
|
||||
import secrets
|
||||
from typing import Optional
|
||||
|
||||
from rio import UserSettings
|
||||
|
||||
from src.ezgg_lan_manager.types.SessionStorage import SessionStorage
|
||||
|
||||
|
||||
class LocalData(UserSettings):
|
||||
stored_session_token: Optional[str] = None
|
||||
|
||||
class LocalDataService:
|
||||
def __init__(self) -> None:
|
||||
self._session: dict[str, SessionStorage] = {}
|
||||
|
||||
def verify_token(self, token: str) -> Optional[SessionStorage]:
|
||||
return self._session.get(token)
|
||||
|
||||
def set_session(self, session: SessionStorage) -> str:
|
||||
key = secrets.token_hex(32)
|
||||
self._session[key] = session
|
||||
return key
|
||||
|
||||
def del_session(self, token: str) -> None:
|
||||
self._session.pop(token, None)
|
||||
@@ -0,0 +1,37 @@
|
||||
import logging
|
||||
from email.message import EmailMessage
|
||||
from asyncio import sleep
|
||||
|
||||
import aiosmtplib
|
||||
|
||||
from src.ezgg_lan_manager.services.ConfigurationService import ConfigurationService
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class MailingService:
|
||||
def __init__(self, configuration_service: ConfigurationService):
|
||||
self._configuration_service = configuration_service
|
||||
self._config = self._configuration_service.get_mailing_service_configuration()
|
||||
|
||||
async def send_email(self, subject: str, body: str, receiver: str) -> None:
|
||||
if self._configuration_service.DEV_MODE_ACTIVE:
|
||||
logger.info(f"Skipped sending mail to {receiver} because demo mode is active.")
|
||||
await sleep(1)
|
||||
return
|
||||
|
||||
try:
|
||||
message = EmailMessage()
|
||||
message["From"] = self._config.sender
|
||||
message["To"] = receiver
|
||||
message["Subject"] = subject
|
||||
message.set_content(body)
|
||||
|
||||
await aiosmtplib.send(
|
||||
message,
|
||||
hostname=self._config.smtp_server,
|
||||
port=self._config.smtp_port,
|
||||
username=self._config.username,
|
||||
password=self._config.password
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email: {e}")
|
||||
@@ -0,0 +1,39 @@
|
||||
import logging
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.types.News import News
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class NewsService:
|
||||
def __init__(self, db_service: DatabaseService) -> None:
|
||||
self._db_service = db_service
|
||||
|
||||
async def add_news(self, news: News) -> None:
|
||||
if news.news_id is not None:
|
||||
logger.warning("Can not add news with ID, ignoring...")
|
||||
return
|
||||
await self._db_service.add_news(news)
|
||||
|
||||
async def get_news(self, dt_start: Optional[date] = None, dt_end: Optional[date] = None, newest_first: bool = True) -> list[News]:
|
||||
if not dt_end:
|
||||
dt_end = date.today()
|
||||
if not dt_start:
|
||||
dt_start = date(1900, 1, 1)
|
||||
fetched_news = await self._db_service.get_news(dt_start, dt_end)
|
||||
return sorted(fetched_news, key=lambda news: news.news_date, reverse=newest_first)
|
||||
|
||||
async def update_news(self, news: News) -> None:
|
||||
return await self._db_service.update_news(news)
|
||||
|
||||
async def delete_news(self, news_id: int) -> None:
|
||||
return await self._db_service.remove_news(news_id)
|
||||
|
||||
async def get_latest_news(self) -> Optional[News]:
|
||||
try:
|
||||
all_news = await self.get_news(None, date.today())
|
||||
return all_news[0]
|
||||
except IndexError:
|
||||
logger.debug("There are no news to fetch")
|
||||
@@ -0,0 +1,48 @@
|
||||
import logging
|
||||
|
||||
import requests
|
||||
|
||||
from src.ezgg_lan_manager.services.SeatingService import SeatingService
|
||||
from src.ezgg_lan_manager.types.CateringOrder import CateringOrder
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import ReceiptPrintingConfiguration
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
logging.getLogger("urllib3").setLevel(logging.FATAL) # Disable logging for urllib3
|
||||
|
||||
class ReceiptPrintingService:
|
||||
def __init__(self, seating_service: SeatingService, config: ReceiptPrintingConfiguration, dev_mode_enabled: bool) -> None:
|
||||
self._seating_service = seating_service
|
||||
self._config = config
|
||||
self._dev_mode_enabled = dev_mode_enabled
|
||||
|
||||
async def print_order(self, user: User, order: CateringOrder) -> None:
|
||||
seat_id = await self._seating_service.get_user_seat(user.user_id)
|
||||
if not seat_id:
|
||||
seat_id = " - "
|
||||
|
||||
menu_items_payload = []
|
||||
for item, amount in order.items.items():
|
||||
menu_items_payload.append({
|
||||
"menu_item_name": item.name,
|
||||
"amount": amount
|
||||
})
|
||||
|
||||
payload = {
|
||||
"order_id": str(order.order_id),
|
||||
"order_date": order.order_date.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z',
|
||||
"customer_name": user.user_name,
|
||||
"seat_id": seat_id,
|
||||
"items": menu_items_payload
|
||||
}
|
||||
try:
|
||||
requests.post(
|
||||
f"http://{self._config.host}:{self._config.port}/{self._config.order_print_endpoint}",
|
||||
json=payload,
|
||||
headers={"x-password": self._config.password}
|
||||
)
|
||||
except Exception as e:
|
||||
if self._dev_mode_enabled:
|
||||
logger.info("An error occurred trying to print a receipt:", e)
|
||||
return
|
||||
logger.error("An error occurred trying to print a receipt:", e)
|
||||
@@ -0,0 +1,62 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.services.TicketingService import TicketingService
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import LanInfo, SeatingConfiguration
|
||||
from src.ezgg_lan_manager.types.Seat import Seat
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
class NoTicketError(Exception):
|
||||
pass
|
||||
|
||||
class SeatNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
class WrongCategoryError(Exception):
|
||||
pass
|
||||
|
||||
class SeatAlreadyTakenError(Exception):
|
||||
pass
|
||||
|
||||
class SeatingService:
|
||||
def __init__(self, lan_info: LanInfo, db_service: DatabaseService, ticketing_service: TicketingService) -> None:
|
||||
self._lan_info = lan_info
|
||||
self._db_service = db_service
|
||||
self._ticketing_service = ticketing_service
|
||||
|
||||
async def get_seating(self) -> list[Seat]:
|
||||
return await self._db_service.get_seating_info()
|
||||
|
||||
async def get_seat(self, seat_id: str, cached_data: Optional[list[Seat]] = None) -> Optional[Seat]:
|
||||
all_seats = await self.get_seating() if not cached_data else cached_data
|
||||
for seat in all_seats:
|
||||
if seat.seat_id == seat_id:
|
||||
return seat
|
||||
|
||||
async def get_user_seat(self, user_id: int) -> Optional[Seat]:
|
||||
all_seats = await self.get_seating()
|
||||
for seat in all_seats:
|
||||
if seat.user and seat.user.user_id == user_id:
|
||||
return seat
|
||||
|
||||
async def seat_user(self, user_id: int, seat_id: str) -> None:
|
||||
user_ticket = await self._ticketing_service.get_user_ticket(user_id)
|
||||
if not user_ticket:
|
||||
raise NoTicketError
|
||||
|
||||
seat = await self.get_seat(seat_id)
|
||||
if not seat:
|
||||
raise SeatNotFoundError
|
||||
|
||||
if seat.category != user_ticket.category:
|
||||
raise WrongCategoryError
|
||||
|
||||
if seat.user is not None:
|
||||
raise SeatAlreadyTakenError
|
||||
|
||||
await self._db_service.seat_user(seat_id, user_id)
|
||||
|
||||
# ToDo: Make function that creates database table `seats` from config
|
||||
@@ -0,0 +1,93 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from src.ezgg_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.types.ConfigurationTypes import TicketInfo
|
||||
from src.ezgg_lan_manager.types.Ticket import Ticket
|
||||
|
||||
logger = logging.getLogger(__name__.split(".")[-1])
|
||||
|
||||
|
||||
class TicketNotAvailableError(Exception):
|
||||
def __init__(self, category: str):
|
||||
self.category = category
|
||||
|
||||
|
||||
class UserAlreadyHasTicketError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class TicketingService:
|
||||
def __init__(self, ticket_infos: tuple[TicketInfo, ...], db_service: DatabaseService,
|
||||
accounting_service: AccountingService) -> None:
|
||||
self._ticket_infos = ticket_infos
|
||||
self._db_service = db_service
|
||||
self._accounting_service = accounting_service
|
||||
|
||||
def get_ticket_info_by_category(self, category: str) -> Optional[TicketInfo]:
|
||||
return next(filter(lambda t: t.category == category, self._ticket_infos), None)
|
||||
|
||||
def get_total_tickets(self) -> int:
|
||||
return sum([t_i.total_tickets for t_i in self._ticket_infos])
|
||||
|
||||
async def get_available_tickets_for_category(self, category: str) -> int:
|
||||
ticket_info = self.get_ticket_info_by_category(category)
|
||||
if not ticket_info or ticket_info.total_tickets < 1:
|
||||
return 0
|
||||
result = ticket_info.total_tickets
|
||||
|
||||
all_tickets = await self._db_service.get_tickets()
|
||||
for ticket in all_tickets:
|
||||
if ticket.category == category:
|
||||
result -= 1
|
||||
|
||||
return result
|
||||
|
||||
async def purchase_ticket(self, user_id: int, category: str) -> Ticket:
|
||||
all_categories = [t_i.category for t_i in self._ticket_infos]
|
||||
if category not in all_categories or (await self.get_available_tickets_for_category(category)) < 1:
|
||||
raise TicketNotAvailableError(category)
|
||||
|
||||
user_balance = await self._accounting_service.get_balance(user_id)
|
||||
|
||||
ticket_info = self.get_ticket_info_by_category(category)
|
||||
if not ticket_info:
|
||||
raise TicketNotAvailableError(category)
|
||||
|
||||
if ticket_info.price > user_balance:
|
||||
raise InsufficientFundsError
|
||||
|
||||
if await self.get_user_ticket(user_id):
|
||||
raise UserAlreadyHasTicketError
|
||||
|
||||
if new_ticket := await self._db_service.generate_ticket_for_user(user_id, category):
|
||||
await self._accounting_service.remove_balance(
|
||||
user_id,
|
||||
ticket_info.price,
|
||||
f"TICKET {new_ticket.ticket_id}"
|
||||
)
|
||||
logger.debug(f"User {user_id} purchased ticket {new_ticket.ticket_id}")
|
||||
return new_ticket
|
||||
|
||||
raise RuntimeError("An unknown error occurred while purchasing ticket")
|
||||
|
||||
async def refund_ticket(self, user_id: int) -> bool:
|
||||
user_ticket = await self.get_user_ticket(user_id)
|
||||
if not user_ticket:
|
||||
return False
|
||||
|
||||
ticket_info = self.get_ticket_info_by_category(user_ticket.category)
|
||||
if await self._db_service.delete_ticket(user_ticket.ticket_id):
|
||||
await self._accounting_service.add_balance(user_id, ticket_info.price,
|
||||
f"TICKET REFUND {user_ticket.ticket_id}")
|
||||
logger.debug(f"User {user_id} refunded ticket {user_ticket.ticket_id}")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
async def transfer_ticket(self, ticket_id: int, user_id: int) -> bool:
|
||||
return await self._db_service.change_ticket_owner(ticket_id, user_id)
|
||||
|
||||
async def get_user_ticket(self, user_id: int) -> Optional[Ticket]:
|
||||
return await self._db_service.get_ticket_for_user(user_id)
|
||||
@@ -0,0 +1,70 @@
|
||||
from hashlib import sha256
|
||||
from typing import Union, Optional
|
||||
from string import ascii_letters, digits
|
||||
|
||||
from src.ezgg_lan_manager.services.DatabaseService import DatabaseService
|
||||
from src.ezgg_lan_manager.types.User import User
|
||||
|
||||
|
||||
class NameNotAllowedError(Exception):
|
||||
def __init__(self, disallowed_char: str) -> None:
|
||||
self.disallowed_char = disallowed_char
|
||||
|
||||
class UserService:
|
||||
ALLOWED_USER_NAME_SYMBOLS = ascii_letters + digits + "!#$%&*+,-./:;<=>?[]^_{|}~"
|
||||
MAX_USERNAME_LENGTH = 14
|
||||
|
||||
def __init__(self, db_service: DatabaseService) -> None:
|
||||
self._db_service = db_service
|
||||
|
||||
async def get_all_users(self) -> list[User]:
|
||||
return await self._db_service.get_all_users()
|
||||
|
||||
async def get_user(self, accessor: Optional[Union[str, int]]) -> Optional[User]:
|
||||
if accessor is None:
|
||||
return
|
||||
if isinstance(accessor, int):
|
||||
return await self._db_service.get_user_by_id(accessor)
|
||||
accessor = accessor.lower()
|
||||
if "@" in accessor:
|
||||
return await self._db_service.get_user_by_mail(accessor)
|
||||
return await self._db_service.get_user_by_name(accessor)
|
||||
|
||||
async def set_profile_picture(self, user_id: int, picture: bytes) -> None:
|
||||
await self._db_service.set_user_profile_picture(user_id, picture)
|
||||
|
||||
async def remove_profile_picture(self, user_id: int) -> None:
|
||||
await self._db_service.remove_profile_picture(user_id)
|
||||
|
||||
async def get_profile_picture(self, user_id: int) -> bytes:
|
||||
return await self._db_service.get_user_profile_picture(user_id)
|
||||
|
||||
async def create_user(self, user_name: str, user_mail: str, password_clear_text: str) -> User:
|
||||
disallowed_char = self._check_for_disallowed_char(user_name)
|
||||
if disallowed_char:
|
||||
raise NameNotAllowedError(disallowed_char)
|
||||
|
||||
user_name = user_name.lower()
|
||||
|
||||
hashed_pw = sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||
created_user = await self._db_service.create_user(user_name, user_mail, hashed_pw)
|
||||
return created_user
|
||||
|
||||
async def update_user(self, user: User) -> User:
|
||||
disallowed_char = self._check_for_disallowed_char(user.user_name)
|
||||
if disallowed_char:
|
||||
raise NameNotAllowedError(disallowed_char)
|
||||
user.user_name = user.user_name.lower()
|
||||
return await self._db_service.update_user(user)
|
||||
|
||||
async def is_login_valid(self, user_name_or_mail: str, password_clear_text: str) -> bool:
|
||||
user = await self.get_user(user_name_or_mail)
|
||||
if not user:
|
||||
return False
|
||||
return user.user_password == sha256(password_clear_text.encode(encoding="utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _check_for_disallowed_char(self, name: str) -> Optional[str]:
|
||||
for c in name:
|
||||
if c not in self.ALLOWED_USER_NAME_SYMBOLS:
|
||||
return c
|
||||
Reference in New Issue
Block a user