prerelease/0.6.0 #1
+2
-1
@@ -43,8 +43,9 @@
|
|||||||
password="Alkohol1"
|
password="Alkohol1"
|
||||||
|
|
||||||
[misc]
|
[misc]
|
||||||
|
base_url="https://ezgg-lan.de" # In dev mode, this is localhost
|
||||||
default_profile_picture="src/elm/assets/img/anon.png"
|
default_profile_picture="src/elm/assets/img/anon.png"
|
||||||
dev_mode_active=true # Supresses E-Mail sending
|
dev_mode_active=true # Supresses E-Mail sending, activates PayPal sandbox API
|
||||||
|
|
||||||
[paypal]
|
[paypal]
|
||||||
client_id_sandbox=""
|
client_id_sandbox=""
|
||||||
|
|||||||
+3
-2
@@ -48,6 +48,7 @@ Icon.register_single_icon(
|
|||||||
|
|
||||||
configuration_service = ConfigurationService(from_root("config.toml"))
|
configuration_service = ConfigurationService(from_root("config.toml"))
|
||||||
database_service = DatabaseService(configuration_service.get_database_configuration())
|
database_service = DatabaseService(configuration_service.get_database_configuration())
|
||||||
|
mailing_service = MailingService(configuration_service)
|
||||||
lan_info = configuration_service.get_lan_info()
|
lan_info = configuration_service.get_lan_info()
|
||||||
|
|
||||||
def is_mobile(self: Session) -> bool:
|
def is_mobile(self: Session) -> bool:
|
||||||
@@ -56,8 +57,8 @@ def is_mobile(self: Session) -> bool:
|
|||||||
Session.is_mobile = is_mobile
|
Session.is_mobile = is_mobile
|
||||||
|
|
||||||
async def on_session_start(session: Session) -> None:
|
async def on_session_start(session: Session) -> None:
|
||||||
# Use this line to fake being any user without having to log in
|
|
||||||
if configuration_service.DEV_MODE_ACTIVE:
|
if configuration_service.DEV_MODE_ACTIVE:
|
||||||
|
# Use this line to fake being any user without having to log in
|
||||||
dev_user = await session[UserService].get_user("Typhus")
|
dev_user = await session[UserService].get_user("Typhus")
|
||||||
if not dev_user:
|
if not dev_user:
|
||||||
logger.fatal("DEV MODE USER DOES NOT EXIST")
|
logger.fatal("DEV MODE USER DOES NOT EXIST")
|
||||||
@@ -78,7 +79,7 @@ app = App(
|
|||||||
theme=theme,
|
theme=theme,
|
||||||
assets_dir=Path(__file__).parent / "assets",
|
assets_dir=Path(__file__).parent / "assets",
|
||||||
build=RootComponent,
|
build=RootComponent,
|
||||||
default_attachments=[LocalData(), configuration_service, database_service, UserService(), LocalDataService(), MailingService(configuration_service), AccountingService(configuration_service.get_paypal_configuration())],
|
default_attachments=[LocalData(), configuration_service, database_service, UserService(), LocalDataService(), mailing_service, AccountingService(configuration_service, mailing_service)],
|
||||||
on_app_start=on_app_start,
|
on_app_start=on_app_start,
|
||||||
on_session_start=on_session_start,
|
on_session_start=on_session_start,
|
||||||
icon=from_root("src/elm/assets/img/favicon.png"),
|
icon=from_root("src/elm/assets/img/favicon.png"),
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
from typing import Optional, Callable
|
from asyncio import sleep
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from rio import Component, Row, Column, Color, PointerEventListener, PointerEvent, Rectangle, Text, TextStyle, Icon, event
|
from rio import Component, Row, Column, Color, PointerEventListener, PointerEvent, Rectangle, Text, TextStyle, event
|
||||||
|
|
||||||
|
from elm.types import UserSession
|
||||||
|
from elm.services import AccountingService
|
||||||
|
|
||||||
|
|
||||||
class UserNavigationButton(Component):
|
class UserNavigationButton(Component):
|
||||||
@@ -61,15 +66,31 @@ class UserNavigationButton(Component):
|
|||||||
|
|
||||||
class UserNavigation(Component):
|
class UserNavigation(Component):
|
||||||
close_navigation: Callable
|
close_navigation: Callable
|
||||||
|
balance: Decimal = Decimal(0)
|
||||||
|
|
||||||
@event.on_page_change
|
@event.on_page_change
|
||||||
async def on_page_change(self) -> None:
|
async def on_page_change(self) -> None:
|
||||||
await self.close_navigation()
|
await self.close_navigation()
|
||||||
|
|
||||||
|
async def update_balance(self) -> None:
|
||||||
|
try:
|
||||||
|
balance = await self.session[AccountingService].get_balance(self.session[UserSession].user_name)
|
||||||
|
if balance != self.balance:
|
||||||
|
self.balance = balance
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await sleep(5)
|
||||||
|
self.session.create_task(self.update_balance())
|
||||||
|
|
||||||
|
@event.on_populate
|
||||||
|
async def on_populate(self) -> None:
|
||||||
|
self.session.create_task(self.update_balance())
|
||||||
|
|
||||||
def build(self) -> Component:
|
def build(self) -> Component:
|
||||||
return Rectangle(
|
return Rectangle(
|
||||||
content=Column(
|
content=Column(
|
||||||
UserNavigationButton("Guthaben: 0,00 €", "/balance", self.close_navigation),
|
UserNavigationButton(f"Guthaben: {self.session[AccountingService].make_euro_string_from_decimal(self.balance)}", "/balance", self.close_navigation),
|
||||||
UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation),
|
UserNavigationButton("Mein Profil", "/my-profile", self.close_navigation),
|
||||||
UserNavigationButton("Mein Clan", "/my-clans", self.close_navigation),
|
UserNavigationButton("Mein Clan", "/my-clans", self.close_navigation),
|
||||||
UserNavigationButton("Ausloggen", "/logout", self.close_navigation)
|
UserNavigationButton("Ausloggen", "/logout", self.close_navigation)
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import date, datetime
|
from asyncio import sleep
|
||||||
|
from datetime import datetime
|
||||||
from decimal import Decimal, ROUND_DOWN
|
from decimal import Decimal, ROUND_DOWN
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, TextInput, GuardEvent, DateInput, PointerEventListener, Revealer, Image, NumberInput
|
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, GuardEvent, Revealer, Image, NumberInput
|
||||||
from rio.event import on_populate
|
from rio.event import on_populate
|
||||||
|
|
||||||
from elm.types import UserSession, Transaction
|
from elm.types import UserSession, Transaction
|
||||||
from elm.services import UserService, AccountingService
|
from elm.services import AccountingService
|
||||||
from elm.components import ElmButton, AvatarEditBox, AccountInfoBox
|
from elm.components import ElmButton
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -84,13 +85,30 @@ class MyBalancePage(Component):
|
|||||||
"DE47517624340019856607",
|
"DE47517624340019856607",
|
||||||
f"AUFLADUNG - {self.session[UserSession].user_name}")
|
f"AUFLADUNG - {self.session[UserSession].user_name}")
|
||||||
|
|
||||||
|
async def check_if_paypal_process_done(self) -> None:
|
||||||
|
await sleep(2)
|
||||||
|
if await self.session[AccountingService].has_user_open_orders(self.session[UserSession].user_name):
|
||||||
|
self.session.create_task(self.check_if_paypal_process_done())
|
||||||
|
else:
|
||||||
|
self.paypal_charge_in_progress = False
|
||||||
|
self.paypal_charge_amount = 0.00
|
||||||
|
self.current_balance = self.session[AccountingService].make_euro_string_from_decimal(
|
||||||
|
await self.session[AccountingService].get_balance(self.session[UserSession].user_name)
|
||||||
|
)
|
||||||
|
self.last_20_transactions = (await self.session[AccountingService].get_transaction_history(self.session[UserSession].user_name))[:20]
|
||||||
|
self.paypal_revealer_open = False
|
||||||
|
|
||||||
async def pay_with_paypal(self) -> None:
|
async def pay_with_paypal(self) -> None:
|
||||||
self.paypal_charge_in_progress = True
|
self.paypal_charge_in_progress = True
|
||||||
logger.info("Starting PayPal transaction over %s for user %s", f"{self.paypal_charge_amount} €", self.session[UserSession].user_name)
|
logger.info("Starting PayPal transaction over %s for user %s", f"{self.paypal_charge_amount} €", self.session[UserSession].user_name)
|
||||||
amount = Decimal(self.paypal_charge_amount)
|
amount = Decimal(self.paypal_charge_amount)
|
||||||
|
try:
|
||||||
approval_url = await self.session[AccountingService].start_paypal_process(self.session[UserSession].user_name, amount)
|
approval_url = await self.session[AccountingService].start_paypal_process(self.session[UserSession].user_name, amount)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
return
|
||||||
self.session.open_url_in_browser(approval_url)
|
self.session.open_url_in_browser(approval_url)
|
||||||
# ToDo: Catch return URL somehow and notify user
|
self.session.create_task(self.check_if_paypal_process_done())
|
||||||
|
|
||||||
async def toggle_bank_revealer(self) -> None:
|
async def toggle_bank_revealer(self) -> None:
|
||||||
self.bank_revealer_open = not self.bank_revealer_open
|
self.bank_revealer_open = not self.bank_revealer_open
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from asyncio import sleep
|
||||||
|
|
||||||
|
from rio import Component, Column, Row, Text, Spacer, page, Rectangle, QueryParameter, ProgressCircle
|
||||||
|
from rio.event import on_populate
|
||||||
|
|
||||||
|
from elm.services import AccountingService
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
|
@page(name="PayPal Return", url_segment="return-paypal")
|
||||||
|
class PayPalReturnPage(Component):
|
||||||
|
token: QueryParameter[str] = "No Value"
|
||||||
|
in_progress: bool = True
|
||||||
|
error_message: str = ""
|
||||||
|
success_message: str = ""
|
||||||
|
|
||||||
|
@on_populate
|
||||||
|
async def on_populate(self) -> None:
|
||||||
|
result = await self.session[AccountingService].finalize_paypal_process(self.token)
|
||||||
|
await sleep(1)
|
||||||
|
if result:
|
||||||
|
self.in_progress = False
|
||||||
|
self.success_message = "Aufladung erfolgreich. Du kannst dieses Fenster schließen."
|
||||||
|
else:
|
||||||
|
self.in_progress = False
|
||||||
|
self.error_message = "Es ist ein Fehler aufgetreten, bitte kontaktiere uns"
|
||||||
|
|
||||||
|
def build(self) -> Component:
|
||||||
|
col_contents = []
|
||||||
|
if self.in_progress:
|
||||||
|
col_contents.append(ProgressCircle(min_size=5, color=self.session.theme.primary_color))
|
||||||
|
col_contents.append(Text("Wir prüfen deine Aufladung", overflow="wrap", justify="center"))
|
||||||
|
else:
|
||||||
|
if self.error_message:
|
||||||
|
col_contents.append(Text(self.error_message, overflow="wrap", justify="center", fill=self.session.theme.danger_color))
|
||||||
|
elif self.success_message:
|
||||||
|
col_contents.append(Text(self.success_message, overflow="wrap", justify="center", fill=self.session.theme.success_color))
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Rectangle(
|
||||||
|
content=Text("Paypal Aufladung", margin=0.5, selectable=False, overflow="wrap"),
|
||||||
|
fill=self.session.theme.header_box_background_color,
|
||||||
|
margin=0.4
|
||||||
|
),
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
*col_contents,
|
||||||
|
margin=1,
|
||||||
|
spacing=1
|
||||||
|
),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
min_width=1 if self.session.is_mobile() else 25
|
||||||
|
),
|
||||||
|
align_x=0.5,
|
||||||
|
align_y=0.5
|
||||||
|
)
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from rio import Component, Column, Row, Text, Spacer, page, Color, TextStyle, Rectangle, TextInput, ProgressBar, Dict
|
||||||
|
from rio.event import on_populate
|
||||||
|
|
||||||
|
from elm.services import ConfigurationService, AccountingService
|
||||||
|
from elm.components import LanCountdownBox, LanInfoBox, LandingPageBoxFull, LandingPageBoxHalf, ElmButton
|
||||||
|
from elm.types import Ticket
|
||||||
|
|
||||||
|
|
||||||
|
@page(name="Tickets", url_segment="tickets")
|
||||||
|
class TicketsPage(Component):
|
||||||
|
sold_tickets_by_category: Dict[str, int] = Dict()
|
||||||
|
|
||||||
|
"""
|
||||||
|
ToDo: Implement conditional ticket buying (check login!)
|
||||||
|
"""
|
||||||
|
|
||||||
|
@on_populate
|
||||||
|
async def on_populate(self) -> None:
|
||||||
|
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
||||||
|
self.sold_tickets_by_category[ticket_info.category] = len(await Ticket.find_many(Ticket.category == ticket_info.category).to_list())
|
||||||
|
|
||||||
|
def get_available_tickets_by_category(self, category: str, total_tickets: int) -> int:
|
||||||
|
return total_tickets - self.sold_tickets_by_category.get(category, 0)
|
||||||
|
|
||||||
|
def build(self) -> Component:
|
||||||
|
row_col = Column if self.session.is_mobile() else Row
|
||||||
|
ticket_boxes = []
|
||||||
|
for ticket_info in self.session[ConfigurationService].get_ticket_info():
|
||||||
|
ticket_boxes.append(
|
||||||
|
Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Column(
|
||||||
|
Rectangle(
|
||||||
|
content=Rectangle(
|
||||||
|
content=Row(
|
||||||
|
Text(ticket_info.description, margin=0.5, selectable=False, overflow="wrap", grow_x=True),
|
||||||
|
Text(self.session[AccountingService].make_euro_string_from_decimal(ticket_info.price), justify="right", margin_right=0.5, fill=self.session.theme.warning_color)
|
||||||
|
),
|
||||||
|
fill=self.session.theme.header_box_background_color,
|
||||||
|
margin=0.4
|
||||||
|
),
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color,
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
Text(ticket_info.additional_info, overflow="wrap", margin_bottom=1),
|
||||||
|
Text("Du besitzt dieses Ticket!", margin_bottom=3, overflow="wrap", fill=self.session.theme.success_color),
|
||||||
|
Row(Text("Verfügbar:", font_size=0.8 if self.session.is_mobile() else 1), Text(f"{self.get_available_tickets_by_category(ticket_info.category, ticket_info.total_tickets)} / {ticket_info.total_tickets}", justify="right", font_size=0.8 if self.session.is_mobile() else 1)),
|
||||||
|
ProgressBar(progress=self.get_available_tickets_by_category(ticket_info.category, ticket_info.total_tickets) / ticket_info.total_tickets, min_height=1),
|
||||||
|
ElmButton(text="Kaufen"),
|
||||||
|
margin=1,
|
||||||
|
spacing=1
|
||||||
|
),
|
||||||
|
Spacer()
|
||||||
|
),
|
||||||
|
fill=self.session.theme.box_color,
|
||||||
|
stroke_width=0.1,
|
||||||
|
stroke_color=self.session.theme.box_border_color
|
||||||
|
),
|
||||||
|
Spacer()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return row_col(
|
||||||
|
*ticket_boxes,
|
||||||
|
spacing=1,
|
||||||
|
margin=1
|
||||||
|
)
|
||||||
@@ -7,6 +7,7 @@ import httpx
|
|||||||
import qrcode
|
import qrcode
|
||||||
|
|
||||||
from elm.types import Transaction, User, PayPalConfiguration
|
from elm.types import Transaction, User, PayPalConfiguration
|
||||||
|
from elm.services import MailingService, ConfigurationService
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
|
|
||||||
@@ -16,16 +17,29 @@ class InsufficientFundsError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class AccountingService:
|
class AccountingService:
|
||||||
def __init__(self, paypal_config: PayPalConfiguration) -> None:
|
PAYPAL_SANDBOX_URL = "https://api-m.sandbox.paypal.com"
|
||||||
self._paypal_config = paypal_config
|
PAYPAL_PROD_URL = "https://api-m.paypal.com"
|
||||||
|
|
||||||
|
def __init__(self, configuration_service: ConfigurationService, mailing_service: MailingService) -> None:
|
||||||
|
self._configuration_service = configuration_service
|
||||||
|
self._paypal_config: PayPalConfiguration = configuration_service.get_paypal_configuration()
|
||||||
|
self._pending_paypal_orders: dict[str, tuple[str, Decimal]] = {}
|
||||||
|
self._mailing_service = mailing_service
|
||||||
|
|
||||||
|
async def has_user_open_orders(self, user_name: str) -> bool:
|
||||||
|
for pending_paypal_order in self._pending_paypal_orders.values():
|
||||||
|
if pending_paypal_order[0] == user_name:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
async def get_paypal_access_token(self) -> str:
|
async def get_paypal_access_token(self) -> str:
|
||||||
|
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"https://api-m.sandbox.paypal.com/v1/oauth2/token",
|
f"{url}/v1/oauth2/token",
|
||||||
auth=(
|
auth=(
|
||||||
self._paypal_config.client_id_sandbox,
|
self._paypal_config.client_id_sandbox if self._configuration_service.DEV_MODE_ACTIVE else self._paypal_config.client_id,
|
||||||
self._paypal_config.secret_sandbox,
|
self._paypal_config.secret_sandbox if self._configuration_service.DEV_MODE_ACTIVE else self._paypal_config.secret,
|
||||||
),
|
),
|
||||||
headers={
|
headers={
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
@@ -41,10 +55,13 @@ class AccountingService:
|
|||||||
return data["access_token"]
|
return data["access_token"]
|
||||||
|
|
||||||
async def start_paypal_process(self, user_name: str, amount: Decimal) -> str:
|
async def start_paypal_process(self, user_name: str, amount: Decimal) -> str:
|
||||||
|
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
|
||||||
|
return_domain = "http://localhost:8000" if self._configuration_service.DEV_MODE_ACTIVE else self._configuration_service.BASE_URL
|
||||||
|
amount = amount.quantize(Decimal(".01"))
|
||||||
async with httpx.AsyncClient() as client:
|
async with httpx.AsyncClient() as client:
|
||||||
access_token = await self.get_paypal_access_token()
|
access_token = await self.get_paypal_access_token()
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
url="https://api-m.sandbox.paypal.com/v2/checkout/orders/",
|
url=f"{url}/v2/checkout/orders/",
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {access_token}"
|
"Authorization": f"Bearer {access_token}"
|
||||||
},
|
},
|
||||||
@@ -58,16 +75,60 @@ class AccountingService:
|
|||||||
"value": str(amount)
|
"value": str(amount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"payment_source": {
|
||||||
|
"paypal": {
|
||||||
|
"experience_context": {
|
||||||
|
"return_url": f"{return_domain}/return-paypal",
|
||||||
|
"cancel_url": f"{return_domain}/cancel-paypal",
|
||||||
|
"user_action": "PAY_NOW",
|
||||||
|
"shipping_preference": "NO_SHIPPING"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
approval_url = next(
|
try:
|
||||||
|
payer_action_url = next(
|
||||||
link["href"]
|
link["href"]
|
||||||
for link in response.json()["links"]
|
for link in response.json()["links"]
|
||||||
if link["rel"] == "approve"
|
if link["rel"] == "payer-action"
|
||||||
)
|
)
|
||||||
return approval_url
|
except StopIteration:
|
||||||
|
logger.error("No payer action url found: %s", response.text)
|
||||||
|
return "#"
|
||||||
|
|
||||||
|
self._pending_paypal_orders[response.json()["id"]] = (user_name, amount)
|
||||||
|
|
||||||
|
return payer_action_url
|
||||||
|
|
||||||
|
async def finalize_paypal_process(self, order_id: str) -> bool:
|
||||||
|
url = self.PAYPAL_SANDBOX_URL if self._configuration_service.DEV_MODE_ACTIVE else self.PAYPAL_PROD_URL
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
access_token = await self.get_paypal_access_token()
|
||||||
|
response = await client.get(
|
||||||
|
url=f"{url}/v2/checkout/orders/{order_id}",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is_approved = response.json()["status"] == "APPROVED"
|
||||||
|
|
||||||
|
if is_approved:
|
||||||
|
response = await client.post(
|
||||||
|
f"{url}/v2/checkout/orders/{order_id}/capture",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is_completed = response.json()["status"] == "COMPLETED"
|
||||||
|
if is_completed:
|
||||||
|
await self.add_balance(self._pending_paypal_orders[order_id][0], self._pending_paypal_orders[order_id][1], "PayPal Aufladung")
|
||||||
|
self._pending_paypal_orders.pop(order_id)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
async def add_balance(self, user_name: str, balance_to_add: Decimal, title: str) -> Decimal:
|
async def add_balance(self, user_name: str, balance_to_add: Decimal, title: str) -> Decimal:
|
||||||
user = await User.find_one(User.user_name == user_name)
|
user = await User.find_one(User.user_name == user_name)
|
||||||
@@ -80,7 +141,13 @@ class AccountingService:
|
|||||||
title=title
|
title=title
|
||||||
).save()
|
).save()
|
||||||
logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user '{user_name}'")
|
logger.debug(f"Added balance of {self.make_euro_string_from_decimal(balance_to_add)} to user '{user_name}'")
|
||||||
return await self.get_balance(user_name)
|
new_balance = await self.get_balance(user_name)
|
||||||
|
await self._mailing_service.send_email(
|
||||||
|
"Dein Guthaben wurde aufgeladen",
|
||||||
|
self._mailing_service.generate_account_balance_added_mail_body(user, balance_to_add, new_balance),
|
||||||
|
user.user_mail
|
||||||
|
)
|
||||||
|
return new_balance
|
||||||
|
|
||||||
async def remove_balance(self, user_name: str, balance_to_remove: Decimal, title: str) -> Decimal:
|
async def remove_balance(self, user_name: str, balance_to_remove: Decimal, title: str) -> Decimal:
|
||||||
current_balance = await self.get_balance(user_name)
|
current_balance = await self.get_balance(user_name)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging
|
import logging
|
||||||
import tomllib
|
import tomllib
|
||||||
|
|
||||||
from from_root import from_root
|
from from_root import from_root
|
||||||
|
|
||||||
from elm.types.ConfigurationTypes import MailingServiceConfiguration, LanInfo, ReceiptPrintingConfiguration, DatabaseConfiguration, PayPalConfiguration
|
from elm.types.ConfigurationTypes import MailingServiceConfiguration, LanInfo, ReceiptPrintingConfiguration, DatabaseConfiguration, PayPalConfiguration, TicketInfo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
logger.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
@@ -46,6 +47,21 @@ class ConfigurationService:
|
|||||||
logger.fatal("Error loading DatabaseConfiguration, exiting...")
|
logger.fatal("Error loading DatabaseConfiguration, exiting...")
|
||||||
sys.exit(1)
|
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"]
|
||||||
|
) for value in self._config["tickets"]])
|
||||||
|
except KeyError as e:
|
||||||
|
logger.debug(e)
|
||||||
|
logger.fatal("Error loading ticket configuration, exiting...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def get_database_configuration(self) -> DatabaseConfiguration:
|
def get_database_configuration(self) -> DatabaseConfiguration:
|
||||||
try:
|
try:
|
||||||
return DatabaseConfiguration(
|
return DatabaseConfiguration(
|
||||||
@@ -117,3 +133,7 @@ class ConfigurationService:
|
|||||||
@property
|
@property
|
||||||
def DEFAULT_PROFILE_PICTURE(self) -> bytes:
|
def DEFAULT_PROFILE_PICTURE(self) -> bytes:
|
||||||
return self._DEFAULT_PROFILE_PICTURE
|
return self._DEFAULT_PROFILE_PICTURE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def BASE_URL(self) -> str:
|
||||||
|
return self._config["misc"]["base_url"]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from beanie import init_beanie
|
|||||||
from pymongo import AsyncMongoClient
|
from pymongo import AsyncMongoClient
|
||||||
from pymongo.asynchronous.collection import AsyncCollection
|
from pymongo.asynchronous.collection import AsyncCollection
|
||||||
|
|
||||||
from elm.types import User, Transaction
|
from elm.types import User, Transaction, Ticket
|
||||||
from elm.types.ConfigurationTypes import DatabaseConfiguration
|
from elm.types.ConfigurationTypes import DatabaseConfiguration
|
||||||
|
|
||||||
logger = logging.getLogger(__name__.split(".")[-1])
|
logger = logging.getLogger(__name__.split(".")[-1])
|
||||||
@@ -33,5 +33,5 @@ class DatabaseService:
|
|||||||
self._users: AsyncCollection = self._database["users"]
|
self._users: AsyncCollection = self._database["users"]
|
||||||
await init_beanie(
|
await init_beanie(
|
||||||
database=self._database,
|
database=self._database,
|
||||||
document_models=[User, Transaction]
|
document_models=[User, Transaction, Ticket]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
|
||||||
class NoSuchCategoryError(Exception):
|
class NoSuchCategoryError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -44,3 +46,11 @@ class PayPalConfiguration:
|
|||||||
secret_sandbox: str
|
secret_sandbox: str
|
||||||
client_id: str
|
client_id: str
|
||||||
secret: str
|
secret: str
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TicketInfo:
|
||||||
|
category: str
|
||||||
|
total_tickets: int
|
||||||
|
price: Decimal
|
||||||
|
description: str
|
||||||
|
additional_info: str
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from beanie import Document, Link
|
||||||
|
|
||||||
|
from elm.types import User
|
||||||
|
|
||||||
|
|
||||||
|
class Ticket(Document):
|
||||||
|
category: str
|
||||||
|
purchase_date: datetime
|
||||||
|
owner: Optional[Link[User]] = None
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
name = "tickets"
|
||||||
@@ -2,3 +2,4 @@ from .User import User
|
|||||||
from .UserSession import UserSession
|
from .UserSession import UserSession
|
||||||
from .ConfigurationTypes import *
|
from .ConfigurationTypes import *
|
||||||
from .Transaction import Transaction
|
from .Transaction import Transaction
|
||||||
|
from .Ticket import Ticket
|
||||||
|
|||||||
Reference in New Issue
Block a user