From 0ca06c244c81f0e93eac5458309da7e2c4866df4 Mon Sep 17 00:00:00 2001 From: tcprod Date: Mon, 3 Feb 2025 14:32:16 +0100 Subject: [PATCH] wip --- .../components/CateringCartItem.py | 2 +- .../components/CateringSelectionItem.py | 2 +- .../components/NewTransactionForm.py | 5 +- .../components/ShoppingCartAndOrders.py | 4 +- .../components/TicketBuyCard.py | 2 +- src/ez_lan_manager/components/UserInfoBox.py | 2 +- .../helpers/create_demo_database_content.py | 123 ++++++++++++------ src/ez_lan_manager/pages/Account.py | 4 +- src/ez_lan_manager/pages/ManageUsersPage.py | 14 +- .../services/AccountingService.py | 41 +++--- .../services/CateringService.py | 2 +- .../services/ConfigurationService.py | 8 +- .../services/DatabaseService.py | 93 +++++++------ .../services/TicketingService.py | 9 +- .../types/ConfigurationTypes.py | 10 +- src/ez_lan_manager/types/Transaction.py | 3 +- testing/unittests/AccountingServiceTests.py | 32 ++--- 17 files changed, 210 insertions(+), 146 deletions(-) diff --git a/src/ez_lan_manager/components/CateringCartItem.py b/src/ez_lan_manager/components/CateringCartItem.py index e34263f..f88e0f2 100644 --- a/src/ez_lan_manager/components/CateringCartItem.py +++ b/src/ez_lan_manager/components/CateringCartItem.py @@ -24,7 +24,7 @@ class CateringCartItem(Component): def build(self) -> rio.Component: return Row( Text(self.ellipsize_string(self.article_name), align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), - Text(AccountingService.make_euro_string_from_int(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), + Text(AccountingService.make_euro_string_from_decimal(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), IconButton(icon="material/close", min_size=2, color=self.session.theme.danger_color, style="plain-text", on_press=lambda: self.remove_item_cb(self.list_id)), proportions=(19, 5, 2) ) diff --git a/src/ez_lan_manager/components/CateringSelectionItem.py b/src/ez_lan_manager/components/CateringSelectionItem.py index e7a5ca9..bcad5fb 100644 --- a/src/ez_lan_manager/components/CateringSelectionItem.py +++ b/src/ez_lan_manager/components/CateringSelectionItem.py @@ -41,7 +41,7 @@ class CateringSelectionItem(Component): content=Column( Row( Text(article_name_top, align_x=0, overflow="wrap", min_width=19, style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), - Text(AccountingService.make_euro_string_from_int(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), + Text(AccountingService.make_euro_string_from_decimal(self.article_price), style=TextStyle(fill=self.session.theme.background_color, font_size=0.9)), IconButton( icon="material/add", min_size=2, diff --git a/src/ez_lan_manager/components/NewTransactionForm.py b/src/ez_lan_manager/components/NewTransactionForm.py index 366fd5e..2cdfc7c 100644 --- a/src/ez_lan_manager/components/NewTransactionForm.py +++ b/src/ez_lan_manager/components/NewTransactionForm.py @@ -1,4 +1,5 @@ from datetime import datetime +from decimal import Decimal from typing import Optional from rio import Component, Column, NumberInput, ThemeContextSwitcher, TextInput, Row, Button, EventHandler @@ -18,7 +19,7 @@ class NewTransactionForm(Component): self.new_transaction_cb, Transaction( user_id=self.user.user_id, - value=round(self.input_value * 100), + value=Decimal(str(self.input_value)), is_debit=True, reference=self.input_reason, transaction_date=datetime.now() @@ -30,7 +31,7 @@ class NewTransactionForm(Component): self.new_transaction_cb, Transaction( user_id=self.user.user_id, - value=round(self.input_value * 100), + value=Decimal(str(self.input_value)), is_debit=False, reference=self.input_reason, transaction_date=datetime.now() diff --git a/src/ez_lan_manager/components/ShoppingCartAndOrders.py b/src/ez_lan_manager/components/ShoppingCartAndOrders.py index e738756..d913cb4 100644 --- a/src/ez_lan_manager/components/ShoppingCartAndOrders.py +++ b/src/ez_lan_manager/components/ShoppingCartAndOrders.py @@ -96,7 +96,7 @@ class ShoppingCartAndOrders(Component): { "Artikel": [item.name for item in order.items.keys()] + ["Gesamtpreis:"], "Anzahl": [item for item in order.items.values()] + [""], - "Preis": [AccountingService.make_euro_string_from_int(item.price) for item in order.items.keys()] + [AccountingService.make_euro_string_from_int(order.price)], + "Preis": [AccountingService.make_euro_string_from_decimal(item.price) for item in order.items.keys()] + [AccountingService.make_euro_string_from_decimal(order.price)], }, show_row_numbers=False ) @@ -158,7 +158,7 @@ class ShoppingCartAndOrders(Component): ), Row( Text( - text=f"Preis: {AccountingService.make_euro_string_from_int(sum(cart_item.price for cart_item in cart))}", + text=f"Preis: {AccountingService.make_euro_string_from_decimal(sum(cart_item.price for cart_item in cart))}", style=TextStyle( fill=self.session.theme.background_color, font_size=0.8 diff --git a/src/ez_lan_manager/components/TicketBuyCard.py b/src/ez_lan_manager/components/TicketBuyCard.py index 2ce608f..44c2871 100644 --- a/src/ez_lan_manager/components/TicketBuyCard.py +++ b/src/ez_lan_manager/components/TicketBuyCard.py @@ -67,7 +67,7 @@ class TicketBuyCard(Component): margin_right=1 ), Row( - Text(f"{AccountingService.make_euro_string_from_int(self.price)}", margin_left=1, margin_top=1, grow_x=True), + Text(f"{AccountingService.make_euro_string_from_decimal(self.price)}", margin_left=1, margin_top=1, grow_x=True), Button( Text("Kaufen", align_x=0.5, margin=0.4), margin_right=1, diff --git a/src/ez_lan_manager/components/UserInfoBox.py b/src/ez_lan_manager/components/UserInfoBox.py index f94c6a6..41a5a14 100644 --- a/src/ez_lan_manager/components/UserInfoBox.py +++ b/src/ez_lan_manager/components/UserInfoBox.py @@ -91,7 +91,7 @@ class UserInfoBox(Component): grow_y=False ), UserInfoBoxButton("Profil bearbeiten", "./edit-profile"), - UserInfoBoxButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_int(self.user_balance)}", "./account"), + UserInfoBoxButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.user_balance)}", "./account"), Button( content=Text("Ausloggen", style=TextStyle(fill=Color.from_hex("02dac5"), font_size=0.6)), shape="rectangle", diff --git a/src/ez_lan_manager/helpers/create_demo_database_content.py b/src/ez_lan_manager/helpers/create_demo_database_content.py index 4ae593c..bd07e7c 100644 --- a/src/ez_lan_manager/helpers/create_demo_database_content.py +++ b/src/ez_lan_manager/helpers/create_demo_database_content.py @@ -1,6 +1,7 @@ # USE THIS ON AN EMPTY DATABASE TO GENERATE DEMO DATA import asyncio from datetime import date +from decimal import Decimal import sys @@ -9,13 +10,16 @@ from src.ez_lan_manager.types.CateringMenuItem import CateringMenuItemCategory from src.ez_lan_manager.types.News import News DEMO_USERS = [ - { "user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred" }, # Gast - { "user_name": "gustav", "user_mail": "gustav@demomail.com", "password_clear_text": "gustav" }, # Gast + Ticket(NORMAL) - { "user_name": "jason", "user_mail": "juergen@demomail.com", "password_clear_text": "jason" }, # Gast + Ticket(NORMAL) + Sitzplatz - { "user_name": "lisa", "user_mail": "lisa@demomail.com", "password_clear_text": "lisa" }, # Teamler - { "user_name": "thomas", "user_mail": "thomas@demomail.com", "password_clear_text": "thomas" } # Teamler + Admin + {"user_name": "manfred", "user_mail": "manfred@demomail.com", "password_clear_text": "manfred"}, # Gast + {"user_name": "gustav", "user_mail": "gustav@demomail.com", "password_clear_text": "gustav"}, + # Gast + Ticket(NORMAL) + {"user_name": "jason", "user_mail": "juergen@demomail.com", "password_clear_text": "jason"}, + # Gast + Ticket(NORMAL) + Sitzplatz + {"user_name": "lisa", "user_mail": "lisa@demomail.com", "password_clear_text": "lisa"}, # Teamler + {"user_name": "thomas", "user_mail": "thomas@demomail.com", "password_clear_text": "thomas"} # Teamler + Admin ] + async def run() -> None: services = init_services() await services[3].init_db_pool() @@ -31,38 +35,47 @@ async def run() -> None: if not input("Generate users? (Y/n): ").lower() == "n": # MANFRED - manfred = await user_service.create_user(DEMO_USERS[0]["user_name"], DEMO_USERS[0]["user_mail"], DEMO_USERS[0]["password_clear_text"]) + manfred = await user_service.create_user(DEMO_USERS[0]["user_name"], DEMO_USERS[0]["user_mail"], + DEMO_USERS[0]["password_clear_text"]) # GUSTAV - gustav = await user_service.create_user(DEMO_USERS[1]["user_name"], DEMO_USERS[1]["user_mail"], DEMO_USERS[1]["password_clear_text"]) - await accounting_service.add_balance(gustav.user_id, 100000, "DEMO EINZAHLUNG") + gustav = await user_service.create_user(DEMO_USERS[1]["user_name"], DEMO_USERS[1]["user_mail"], + DEMO_USERS[1]["password_clear_text"]) + await accounting_service.add_balance(gustav.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG") await ticket_service.purchase_ticket(gustav.user_id, "NORMAL") # JASON - jason = await user_service.create_user(DEMO_USERS[2]["user_name"], DEMO_USERS[2]["user_mail"], DEMO_USERS[2]["password_clear_text"]) - await accounting_service.add_balance(jason.user_id, 100000, "DEMO EINZAHLUNG") + jason = await user_service.create_user(DEMO_USERS[2]["user_name"], DEMO_USERS[2]["user_mail"], + DEMO_USERS[2]["password_clear_text"]) + await accounting_service.add_balance(jason.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG") await ticket_service.purchase_ticket(jason.user_id, "NORMAL") await seating_service.seat_user(jason.user_id, "D10") # LISA - lisa = await user_service.create_user(DEMO_USERS[3]["user_name"], DEMO_USERS[3]["user_mail"], DEMO_USERS[3]["password_clear_text"]) - await accounting_service.add_balance(lisa.user_id, 100000, "DEMO EINZAHLUNG") + lisa = await user_service.create_user(DEMO_USERS[3]["user_name"], DEMO_USERS[3]["user_mail"], + DEMO_USERS[3]["password_clear_text"]) + await accounting_service.add_balance(lisa.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG") lisa.is_team_member = True await user_service.update_user(lisa) # THOMAS - thomas = await user_service.create_user(DEMO_USERS[4]["user_name"], DEMO_USERS[4]["user_mail"], DEMO_USERS[4]["password_clear_text"]) - await accounting_service.add_balance(thomas.user_id, 100000, "DEMO EINZAHLUNG") + thomas = await user_service.create_user(DEMO_USERS[4]["user_name"], DEMO_USERS[4]["user_mail"], + DEMO_USERS[4]["password_clear_text"]) + await accounting_service.add_balance(thomas.user_id, Decimal("1000.00"), "DEMO EINZAHLUNG") thomas.is_team_member = True thomas.is_admin = True await user_service.update_user(thomas) if not input("Generate catering menu? (Y/n): ").lower() == "n": # MAIN_COURSE - await catering_service.add_menu_item("Schnitzel Wiener Art", "mit Pommes", 1050, CateringMenuItemCategory.MAIN_COURSE) - await catering_service.add_menu_item("Jäger Schnitzel mit Champignonrahm Sauce", "mit Pommes", 1150, CateringMenuItemCategory.MAIN_COURSE) - await catering_service.add_menu_item("Tortellini in Käsesauce mit Fleischfüllung", "", 1050, CateringMenuItemCategory.MAIN_COURSE) - await catering_service.add_menu_item("Tortellini in Käsesauce ohne Fleischfüllung", "Vegetarisch", 1050, CateringMenuItemCategory.MAIN_COURSE) + await catering_service.add_menu_item("Schnitzel Wiener Art", "mit Pommes", 1050, + CateringMenuItemCategory.MAIN_COURSE) + await catering_service.add_menu_item("Jäger Schnitzel mit Champignonrahm Sauce", "mit Pommes", 1150, + CateringMenuItemCategory.MAIN_COURSE) + await catering_service.add_menu_item("Tortellini in Käsesauce mit Fleischfüllung", "", 1050, + CateringMenuItemCategory.MAIN_COURSE) + await catering_service.add_menu_item("Tortellini in Käsesauce ohne Fleischfüllung", "Vegetarisch", 1050, + CateringMenuItemCategory.MAIN_COURSE) # SNACK await catering_service.add_menu_item("Käse Schinken Wrap", "", 500, CateringMenuItemCategory.SNACK) @@ -93,37 +106,58 @@ async def run() -> None: await catering_service.add_menu_item("Smacks", "", 150, CateringMenuItemCategory.BREAKFAST) await catering_service.add_menu_item("Knuspermüsli", "Schoko", 200, CateringMenuItemCategory.BREAKFAST) await catering_service.add_menu_item("Cini Minis", "", 150, CateringMenuItemCategory.BREAKFAST) - await catering_service.add_menu_item("Brötchen - Schinken", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST) - await catering_service.add_menu_item("Brötchen - Käse", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST) - await catering_service.add_menu_item("Brötchen - Schinken/Käse", "mit Margarine", 140, CateringMenuItemCategory.BREAKFAST) - await catering_service.add_menu_item("Brötchen - Salami", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST) - await catering_service.add_menu_item("Brötchen - Salami/Käse", "mit Margarine", 140, CateringMenuItemCategory.BREAKFAST) - await catering_service.add_menu_item("Brötchen - Nutella", "mit Margarine", 120, CateringMenuItemCategory.BREAKFAST) + await catering_service.add_menu_item("Brötchen - Schinken", "mit Margarine", 120, + CateringMenuItemCategory.BREAKFAST) + await catering_service.add_menu_item("Brötchen - Käse", "mit Margarine", 120, + CateringMenuItemCategory.BREAKFAST) + await catering_service.add_menu_item("Brötchen - Schinken/Käse", "mit Margarine", 140, + CateringMenuItemCategory.BREAKFAST) + await catering_service.add_menu_item("Brötchen - Salami", "mit Margarine", 120, + CateringMenuItemCategory.BREAKFAST) + await catering_service.add_menu_item("Brötchen - Salami/Käse", "mit Margarine", 140, + CateringMenuItemCategory.BREAKFAST) + await catering_service.add_menu_item("Brötchen - Nutella", "mit Margarine", 120, + CateringMenuItemCategory.BREAKFAST) # BEVERAGE_NON_ALCOHOLIC - await catering_service.add_menu_item("Wasser - Still", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) - await catering_service.add_menu_item("Wasser - Medium", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) - await catering_service.add_menu_item("Wasser - Spritzig", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) - await catering_service.add_menu_item("Coca-Cola", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) - await catering_service.add_menu_item("Coca-Cola Zero", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) - await catering_service.add_menu_item("Fanta", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) - await catering_service.add_menu_item("Sprite", "1L Flasche", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) - await catering_service.add_menu_item("Spezi", "von Paulaner, 0,5L Flasche", 150, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Wasser - Still", "1L Flasche", 200, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Wasser - Medium", "1L Flasche", 200, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Wasser - Spritzig", "1L Flasche", 200, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Coca-Cola", "1L Flasche", 200, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Coca-Cola Zero", "1L Flasche", 200, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Fanta", "1L Flasche", 200, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Sprite", "1L Flasche", 200, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Spezi", "von Paulaner, 0,5L Flasche", 150, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) await catering_service.add_menu_item("Red Bull", "", 200, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) - await catering_service.add_menu_item("Energy", "Hausmarke", 150, CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) + await catering_service.add_menu_item("Energy", "Hausmarke", 150, + CateringMenuItemCategory.BEVERAGE_NON_ALCOHOLIC) # BEVERAGE_ALCOHOLIC await catering_service.add_menu_item("Pils", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) - await catering_service.add_menu_item("Radler", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) - await catering_service.add_menu_item("Diesel", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) - await catering_service.add_menu_item("Apfelwein Pur", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) - await catering_service.add_menu_item("Apfelwein Sauer", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) - await catering_service.add_menu_item("Apfelwein Cola", "0,33L Flasche", 190, CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) + await catering_service.add_menu_item("Radler", "0,33L Flasche", 190, + CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) + await catering_service.add_menu_item("Diesel", "0,33L Flasche", 190, + CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) + await catering_service.add_menu_item("Apfelwein Pur", "0,33L Flasche", 190, + CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) + await catering_service.add_menu_item("Apfelwein Sauer", "0,33L Flasche", 190, + CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) + await catering_service.add_menu_item("Apfelwein Cola", "0,33L Flasche", 190, + CateringMenuItemCategory.BEVERAGE_ALCOHOLIC) # BEVERAGE_COCKTAIL await catering_service.add_menu_item("Vodka Energy", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Vodka O-Saft", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL) - await catering_service.add_menu_item("Whiskey Cola", "mit Bourbon", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL) + await catering_service.add_menu_item("Whiskey Cola", "mit Bourbon", 400, + CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Jägermeister Energy", "", 400, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Sex on the Beach", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL) await catering_service.add_menu_item("Long Island Ice Tea", "", 550, CateringMenuItemCategory.BEVERAGE_COCKTAIL) @@ -132,11 +166,13 @@ async def run() -> None: # BEVERAGE_SHOT await catering_service.add_menu_item("Jägermeister", "", 200, CateringMenuItemCategory.BEVERAGE_SHOT) await catering_service.add_menu_item("Tequila", "", 200, CateringMenuItemCategory.BEVERAGE_SHOT) - await catering_service.add_menu_item("PfEZzi", "Getunter Pfefferminz-Schnaps", 199, CateringMenuItemCategory.BEVERAGE_SHOT) + await catering_service.add_menu_item("PfEZzi", "Getunter Pfefferminz-Schnaps", 199, + CateringMenuItemCategory.BEVERAGE_SHOT) # NON_FOOD await catering_service.add_menu_item("Zigaretten", "Elixyr", 800, CateringMenuItemCategory.NON_FOOD) - await catering_service.add_menu_item("Mentholfilter", "passend für Elixyr", 120, CateringMenuItemCategory.NON_FOOD) + await catering_service.add_menu_item("Mentholfilter", "passend für Elixyr", 120, + CateringMenuItemCategory.NON_FOOD) if not input("Generate default new post? (Y/n): ").lower() == "n": loops = 0 @@ -154,11 +190,14 @@ async def run() -> None: news_id=None, title="Der EZ LAN Manager", subtitle="Eine Software des EZ GG e.V.", - content="Dies ist eine WIP-Version des EZ LAN Managers. Diese Software soll uns helfen in Zukunft die LAN Parties des EZ GG e.V.'s zu organisieren. Wer Fehler findet darf sie behalten. (Oder er meldet sie)", + content="Dies ist eine WIP-Version des EZ LAN Managers. Diese Software soll uns helfen in Zukunft die LAN " + "Parties des EZ GG e.V.'s zu organisieren. Wer Fehler findet darf sie behalten. (Oder er meldet " + "sie)", author=user, news_date=date.today() )) + if __name__ == "__main__": with asyncio.Runner() as loop: loop.run(run()) diff --git a/src/ez_lan_manager/pages/Account.py b/src/ez_lan_manager/pages/Account.py index e68588e..beda11c 100644 --- a/src/ez_lan_manager/pages/Account.py +++ b/src/ez_lan_manager/pages/Account.py @@ -159,7 +159,7 @@ class AccountPage(Component): align_x=0 ), Text( - f"{'-' if transaction.is_debit else '+'}{AccountingService.make_euro_string_from_int(transaction.value)}", + f"{'-' if transaction.is_debit else '+'}{AccountingService.make_euro_string_from_decimal(transaction.value)}", style=TextStyle( fill=self.session.theme.danger_color if transaction.is_debit else self.session.theme.success_color, font_size=0.8 @@ -175,7 +175,7 @@ class AccountPage(Component): return Column( MainViewContentBox( content=Text( - f"Kontostand: {AccountingService.make_euro_string_from_int(self.balance)}", + f"Kontostand: {AccountingService.make_euro_string_from_decimal(self.balance)}", style=TextStyle( fill=self.session.theme.background_color, font_size=1.2 diff --git a/src/ez_lan_manager/pages/ManageUsersPage.py b/src/ez_lan_manager/pages/ManageUsersPage.py index fe6e812..d95005f 100644 --- a/src/ez_lan_manager/pages/ManageUsersPage.py +++ b/src/ez_lan_manager/pages/ManageUsersPage.py @@ -17,6 +17,7 @@ from src.ez_lan_manager.types.User import User logger = logging.getLogger(__name__.split(".")[-1]) + class ClickableGridContent(Component): text: str = "" is_hovered: bool = False @@ -36,7 +37,8 @@ class ClickableGridContent(Component): content=Rectangle( content=Text( self.text, - style=TextStyle(fill=self.session.theme.success_color) if self.is_hovered else TextStyle(fill=self.session.theme.background_color), + style=TextStyle(fill=self.session.theme.success_color) if self.is_hovered else TextStyle( + fill=self.session.theme.background_color), grow_x=True ), fill=Color.TRANSPARENT, @@ -47,6 +49,7 @@ class ClickableGridContent(Component): on_press=self.on_mouse_click ) + class ManageUsersPage(Component): selected_user: Optional[User] = None all_users: Optional[list] = None @@ -66,13 +69,15 @@ class ManageUsersPage(Component): async def on_user_clicked(self, user_name: str) -> None: self.selected_user = next(filter(lambda user: user.user_name == user_name, self.all_users)) user_account_balance_raw = await self.session[AccountingService].get_balance(self.selected_user.user_id) - self.user_account_balance = AccountingService.make_euro_string_from_int(user_account_balance_raw) + self.user_account_balance = AccountingService.make_euro_string_from_decimal(user_account_balance_raw) seat = await self.session[SeatingService].get_user_seat(self.selected_user.user_id) self.user_seat = seat.seat_id if seat else "-" self.is_user_account_locked = not self.selected_user.is_active async def on_search_parameters_changed(self, e: TextInputChangeEvent) -> None: - self.search_results = list(filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id), self.all_users)) + self.search_results = list( + filter(lambda user: (e.text.lower() in user.user_name.lower()) or e.text.lower() in str(user.user_id), + self.all_users)) async def change_account_active(self, _: SwitchChangeEvent) -> None: self.selected_user.is_active = not self.is_user_account_locked @@ -84,7 +89,7 @@ class ManageUsersPage(Component): logger.info(f"Got new transaction for user with ID '{transaction.user_id}' over " f"{'-' if transaction.is_debit else '+'}" - f"{AccountingService.make_euro_string_from_int(transaction.value)} " + f"{AccountingService.make_euro_string_from_decimal(transaction.value)} " f"with reference '{transaction.reference}'") if transaction.is_debit: @@ -108,7 +113,6 @@ class ManageUsersPage(Component): self.accounting_section_result_text = f"Guthaben {'entfernt' if transaction.is_debit else 'hinzugefügt'}!" self.accounting_section_result_success = True - def build(self) -> Component: return Column( MainViewContentBox( diff --git a/src/ez_lan_manager/services/AccountingService.py b/src/ez_lan_manager/services/AccountingService.py index 735fea1..b179094 100644 --- a/src/ez_lan_manager/services/AccountingService.py +++ b/src/ez_lan_manager/services/AccountingService.py @@ -1,15 +1,18 @@ import logging from collections.abc import Callable from datetime import datetime +from decimal import Decimal, ROUND_DOWN from src.ez_lan_manager.services.DatabaseService import DatabaseService from src.ez_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 @@ -19,7 +22,7 @@ class AccountingService: """ 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: int, reference: str) -> int: + 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, @@ -27,12 +30,12 @@ class AccountingService: reference=reference, transaction_date=datetime.now() )) - logger.debug(f"Added balance of {self.make_euro_string_from_int(balance_to_add)} to user with ID {user_id}") + 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: int, reference: str) -> int: + 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 @@ -43,13 +46,14 @@ class AccountingService: reference=reference, transaction_date=datetime.now() )) - logger.debug(f"Removed balance of {self.make_euro_string_from_int(balance_to_remove)} to user with ID {user_id}") + 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) -> int: - balance_buffer = 0 + 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 @@ -61,23 +65,8 @@ class AccountingService: return await self._db_service.get_all_transactions_for_user(user_id) @staticmethod - def make_euro_string_from_int(cent_int: int) -> str: - """ Internally, all money values are cents as ints. Only when showing them to the user we generate a string. Prevents float inaccuracy. """ - as_str = str(cent_int) - if as_str[0] == "-": - is_negative = True - as_str = as_str[1:] - else: - is_negative = False - - if len(as_str) == 1: - result = f"0.0{as_str} €" - elif len(as_str) == 2: - result = f"0.{as_str} €" - else: - result = f"{as_str[:-2]}.{as_str[-2:]} €" - - if is_negative: - result = f"-{result}" - - return result + def make_euro_string_from_decimal(euros: Decimal) -> str: + """ Internally, all money values are cents as ints. Only when showing them to the user we generate a string. + Prevents float inaccuracy.""" + rounded_decimal = str(euros.quantize(Decimal(".01"), rounding=ROUND_DOWN)) + return f"{rounded_decimal} €" diff --git a/src/ez_lan_manager/services/CateringService.py b/src/ez_lan_manager/services/CateringService.py index 25e6a72..80efa47 100644 --- a/src/ez_lan_manager/services/CateringService.py +++ b/src/ez_lan_manager/services/CateringService.py @@ -46,7 +46,7 @@ class CateringService: 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_int(total_price)}") + 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.cancel_order(order) # ToDo: Check if commented out before commit. Un-comment to auto-cancel every placed order return order diff --git a/src/ez_lan_manager/services/ConfigurationService.py b/src/ez_lan_manager/services/ConfigurationService.py index b5719c7..2700131 100644 --- a/src/ez_lan_manager/services/ConfigurationService.py +++ b/src/ez_lan_manager/services/ConfigurationService.py @@ -1,15 +1,18 @@ 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.ez_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, SeatingConfiguration, TicketInfo +from src.ez_lan_manager.types.ConfigurationTypes import DatabaseConfiguration, MailingServiceConfiguration, LanInfo, \ + SeatingConfiguration, TicketInfo logger = logging.getLogger(__name__.split(".")[-1]) + class ConfigurationService: def __init__(self, config_file_path: Path) -> None: try: @@ -40,7 +43,6 @@ class ConfigurationService: logger.fatal("Error loading DatabaseConfiguration, exiting...") sys.exit(1) - def get_mailing_service_configuration(self) -> MailingServiceConfiguration: try: mailing_configuration = self._config["mailing"] @@ -83,7 +85,7 @@ class ConfigurationService: return tuple([TicketInfo( category=value, total_tickets=self._config["tickets"][value]["total_tickets"], - price=self._config["tickets"][value]["price"], + 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"] diff --git a/src/ez_lan_manager/services/DatabaseService.py b/src/ez_lan_manager/services/DatabaseService.py index dd8619f..1e5c755 100644 --- a/src/ez_lan_manager/services/DatabaseService.py +++ b/src/ez_lan_manager/services/DatabaseService.py @@ -2,6 +2,7 @@ import logging from datetime import date, datetime from typing import Optional +from decimal import Decimal import aiomysql @@ -17,14 +18,18 @@ from src.ez_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 @@ -85,7 +90,6 @@ class DatabaseService: 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: @@ -110,7 +114,7 @@ class DatabaseService: try: await cursor.execute( "INSERT INTO users (user_name, user_mail, user_password) " - "VALUES (%s, %s, %s)", (user_name, user_mail.lower(), password_hash) + "VALUES (%s, %s, %s)", (user_name, user_mail.lower(), password_hash) ) await conn.commit() except aiomysql.InterfaceError: @@ -123,19 +127,19 @@ class DatabaseService: 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) + "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: @@ -155,7 +159,8 @@ class DatabaseService: 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) + (transaction.user_id, transaction.value, transaction.is_debit, transaction.transaction_date, + transaction.reference) ) await conn.commit() except aiomysql.InterfaceError: @@ -189,14 +194,13 @@ class DatabaseService: for transaction_raw in result: transactions.append(Transaction( user_id=user_id, - value=int(transaction_raw[2]), + 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: @@ -215,13 +219,15 @@ class DatabaseService: 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 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() @@ -315,12 +321,13 @@ class DatabaseService: 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 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() @@ -347,7 +354,8 @@ class DatabaseService: 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 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() @@ -360,12 +368,12 @@ class DatabaseService: 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)) + 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: @@ -382,7 +390,7 @@ class DatabaseService: 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 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() @@ -401,7 +409,8 @@ class DatabaseService: 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 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() @@ -417,7 +426,8 @@ class DatabaseService: 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 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() @@ -486,7 +496,8 @@ class DatabaseService: 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 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() @@ -501,20 +512,22 @@ class DatabaseService: if raw_data is None: return return CateringMenuItem( - item_id=raw_data[0], - name=raw_data[1], - additional_info=raw_data[2], - price=raw_data[3], - category=CateringMenuItemCategory(raw_data[4]), - is_disabled=bool(raw_data[5]) - ) + item_id=raw_data[0], + name=raw_data[1], + additional_info=raw_data[2], + price=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: int, category: CateringMenuItemCategory, is_disabled: bool = False) -> Optional[CateringMenuItem]: + async def add_menu_item(self, name: str, info: str, price: int, 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);", + "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() @@ -540,7 +553,8 @@ class DatabaseService: 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 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() @@ -557,8 +571,10 @@ class DatabaseService: 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) + "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() @@ -584,7 +600,8 @@ class DatabaseService: 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);", + "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() @@ -673,7 +690,7 @@ class DatabaseService: "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, ) + (order_id,) ) await conn.commit() except aiomysql.InterfaceError: @@ -718,7 +735,7 @@ class DatabaseService: 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 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: diff --git a/src/ez_lan_manager/services/TicketingService.py b/src/ez_lan_manager/services/TicketingService.py index d597e9b..4a0e6de 100644 --- a/src/ez_lan_manager/services/TicketingService.py +++ b/src/ez_lan_manager/services/TicketingService.py @@ -8,15 +8,19 @@ from src.ez_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: + 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 @@ -75,7 +79,8 @@ class TicketingService: 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}") + 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 diff --git a/src/ez_lan_manager/types/ConfigurationTypes.py b/src/ez_lan_manager/types/ConfigurationTypes.py index c888180..17ce86e 100644 --- a/src/ez_lan_manager/types/ConfigurationTypes.py +++ b/src/ez_lan_manager/types/ConfigurationTypes.py @@ -1,11 +1,13 @@ from dataclasses import dataclass from datetime import datetime -from pathlib import Path + +from decimal import Decimal class NoSuchCategoryError(Exception): pass + @dataclass(frozen=True) class DatabaseConfiguration: db_user: str @@ -14,15 +16,17 @@ class DatabaseConfiguration: db_port: int db_name: str + @dataclass(frozen=True) class TicketInfo: category: str total_tickets: int - price: int + price: Decimal description: str additional_info: str is_default: bool + @dataclass(frozen=True) class MailingServiceConfiguration: smtp_server: str @@ -31,6 +35,7 @@ class MailingServiceConfiguration: username: str password: str + @dataclass(frozen=True) class LanInfo: name: str @@ -39,6 +44,7 @@ class LanInfo: date_till: datetime organizer_mail: str + @dataclass(frozen=True) class SeatingConfiguration: seats: dict[str, str] diff --git a/src/ez_lan_manager/types/Transaction.py b/src/ez_lan_manager/types/Transaction.py index 18758cd..1cade70 100644 --- a/src/ez_lan_manager/types/Transaction.py +++ b/src/ez_lan_manager/types/Transaction.py @@ -1,11 +1,12 @@ from dataclasses import dataclass from datetime import datetime +from decimal import Decimal @dataclass(frozen=True) class Transaction: user_id: int - value: int + value: Decimal is_debit: bool reference: str transaction_date: datetime diff --git a/testing/unittests/AccountingServiceTests.py b/testing/unittests/AccountingServiceTests.py index cc1a722..0dfd59a 100644 --- a/testing/unittests/AccountingServiceTests.py +++ b/testing/unittests/AccountingServiceTests.py @@ -1,6 +1,7 @@ import unittest from datetime import datetime from unittest.mock import MagicMock, AsyncMock +from decimal import Decimal from src.ez_lan_manager.services.AccountingService import AccountingService, InsufficientFundsError from src.ez_lan_manager.types.Transaction import Transaction @@ -12,7 +13,6 @@ class AccountingServiceTests(unittest.IsolatedAsyncioTestCase): self.mock_database_service.add_transaction = AsyncMock() self.accounting_service = AccountingService(self.mock_database_service) - def test_importing_unit_under_test_works(self) -> None: """ This test asserts that the object produced in setUp is AccountingService object, @@ -21,59 +21,59 @@ class AccountingServiceTests(unittest.IsolatedAsyncioTestCase): self.assertIsInstance(self.accounting_service, AccountingService) def test_making_string_from_euro_value_works_correctly(self) -> None: - test_value = 13466 + test_value = Decimal("134.66") expected_result = "134.66 €" - self.assertEqual(expected_result, AccountingService.make_euro_string_from_int(test_value)) + self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value)) def test_making_euro_string_from_negative_value_works_correctly(self) -> None: - test_value = -99741 + test_value = Decimal("-997.41") expected_result = "-997.41 €" - self.assertEqual(expected_result, AccountingService.make_euro_string_from_int(test_value)) + self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value)) def test_making_euro_string_from_less_than_ten_cents_works_correctly(self) -> None: - test_value = 4 + test_value = Decimal("0.04") expected_result = "0.04 €" - self.assertEqual(expected_result, AccountingService.make_euro_string_from_int(test_value)) + self.assertEqual(expected_result, AccountingService.make_euro_string_from_decimal(test_value)) async def test_get_balance_correctly_adds_up_transactions(self) -> None: self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[ Transaction( user_id=0, - value=5, + value=Decimal("0.05"), is_debit=True, reference="", transaction_date=datetime.now() ), Transaction( user_id=0, - value=99, + value=Decimal("0.99"), is_debit=False, reference="", transaction_date=datetime.now() ), Transaction( user_id=0, - value=101, + value=Decimal("1.01"), is_debit=False, reference="", transaction_date=datetime.now() ), Transaction( user_id=0, - value=77, + value=Decimal("0.77"), is_debit=True, reference="", transaction_date=datetime.now() ), ]) - expected_result = 118 + expected_result = Decimal("1.18") actual_result = await self.accounting_service.get_balance(0) self.assertEqual(expected_result, actual_result) async def test_trying_to_remove_more_than_is_on_account_balance_raises_exception(self) -> None: - user_balance = 100 - balance_to_remove = 101 + user_balance = Decimal("1.00") + balance_to_remove = Decimal("1.01") self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[ Transaction( user_id=0, @@ -88,8 +88,8 @@ class AccountingServiceTests(unittest.IsolatedAsyncioTestCase): await self.accounting_service.remove_balance(0, balance_to_remove, "TestRef") async def test_trying_to_remove_less_than_is_on_account_balance_spawns_correct_transaction(self) -> None: - user_balance = 101 - balance_to_remove = 100 + user_balance = Decimal("1.01") + balance_to_remove = Decimal("1.00") reference = "Yey, a reference" self.mock_database_service.get_all_transactions_for_user = AsyncMock(return_value=[