Add transaction table and prepare drink popup

This commit is contained in:
2025-06-04 02:26:01 +02:00
parent 48e34008ed
commit 96c7f86e3d
3 changed files with 190 additions and 9 deletions

View File

@@ -27,7 +27,7 @@ engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
DRINK_COST = 100 # cent
with engine.connect() as conn:
# Create a new table for postpaid users
# Create a table for postpaid users
conn.execute(text("""
CREATE TABLE IF NOT EXISTS users_postpaid (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -39,6 +39,7 @@ with engine.connect() as conn:
)
"""))
# create a table for every prepaid user
conn.execute(text("""
CREATE TABLE IF NOT EXISTS users_prepaid (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -52,6 +53,7 @@ with engine.connect() as conn:
)
"""))
# create a table for every push on the drink button
conn.execute(text("""
CREATE TABLE IF NOT EXISTS drinks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -63,10 +65,91 @@ with engine.connect() as conn:
FOREIGN KEY (prepaid_user_id) REFERENCES users_prepaid(id)
)
"""))
# create a table for every money transaction
conn.execute(text("""
CREATE TABLE IF NOT EXISTS transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
postpaid_user_id INTEGER,
prepaid_user_id INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
previous_money INT NOT NULL,
new_money INT NOT NULL,
delta_money INT NOT NULL,
description TEXT,
FOREIGN KEY (postpaid_user_id) REFERENCES users_postpaid(id),
FOREIGN KEY (prepaid_user_id) REFERENCES users_prepaid(id)
)
"""))
conn.commit()
def _log_transaction(
user_id: int,
user_is_postpaid: bool,
previous_money_cent = None,
new_money_cent = None,
delta_money_cent = None,
description = None
):
"""
Logs a transaction for a user, recording changes in their account balance.
Depending on whether the user is postpaid or prepaid, retrieves the previous balance if not provided,
calculates the new balance and delta if necessary, and inserts a transaction record into the database.
Args:
user_id (int): The ID of the user for whom the transaction is being logged.
user_is_postpaid (bool): True if the user is postpaid, False if prepaid.
previous_money_cent (Optional[int], default=None): The user's previous balance in cents. If None, it is fetched from the database.
new_money_cent (Optional[int], default=None): The user's new balance in cents. If None, it is calculated using delta_money_cent.
delta_money_cent (Optional[int], default=None): The change in balance in cents. If None, it is calculated using new_money_cent.
description (Optional[str], default=None): A description of the transaction.
Raises:
HTTPException: If the user is not found, if both new_money_cent and delta_money_cent are missing,
or if the transaction could not be logged.
Returns:
int: The ID of the newly created transaction record.
"""
if previous_money_cent is None:
if user_is_postpaid:
t_get_prev_money = text("SELECT money FROM users_postpaid WHERE id = :id")
else:
t_get_prev_money = text("SELECT money FROM users_prepaid WHERE id = :id")
with engine.connect() as connection:
res = connection.execute(t_get_prev_money, {"id": user_id}).fetchone()
if res:
previous_money_cent = int(res[0])
else:
raise HTTPException(status_code=404, detail="User not found")
if new_money_cent is None and delta_money_cent is None:
raise HTTPException(status_code=400, detail="Either new_money or delta_money must be provided, not both")
if new_money_cent is None and delta_money_cent is not None:
new_money_cent = previous_money_cent + delta_money_cent
elif delta_money_cent is None and new_money_cent is not None:
delta_money_cent = new_money_cent - previous_money_cent
# here we definitly have all the variables
if user_is_postpaid:
t = text("INSERT INTO transactions (postpaid_user_id, previous_money, new_money, delta_money, description) VALUES (:user_id, :previous_money, :new_money, :delta_money, :description)")
else:
t = text("INSERT INTO transactions (prepaid_user_id, previous_money, new_money, delta_money, description) VALUES (:user_id, :previous_money, :new_money, :delta_money, :description)")
with engine.connect() as connection:
result = connection.execute(
t,
{
"user_id": user_id,
"previous_money": previous_money_cent,
"new_money": new_money_cent,
"delta_money": delta_money_cent,
"description": description
}
)
if result.rowcount == 0:
raise HTTPException(status_code=500, detail="Failed to log transaction")
connection.commit()
return result.lastrowid
def create_postpaid_user(username: str):
"""
Creates a new postpaid user with the given username in the users_postpaid table.
@@ -105,7 +188,7 @@ def get_postpaid_user(user_id: int):
Raises:
HTTPException: If no user with the given ID is found, raises a 404 HTTPException.
"""
t = text("SELECT id, username, money, activated, last_drink FROM users_postpaid WHERE id = :id")
user_db = {}
with engine.connect() as connection:
@@ -130,7 +213,7 @@ def get_postpaid_user_by_username(username: str):
Raises:
HTTPException: If no user with the given username is found, raises a 404 HTTPException.
"""
t = text("SELECT id, username, money, activated, last_drink FROM users_postpaid WHERE username = :username")
user_db = {}
with engine.connect() as connection:
@@ -158,6 +241,7 @@ def set_postpaid_user_money(user_id: int, money: float):
"""
print(f"set_postpaid_user_money: {user_id}, {money}")
_log_transaction(user_id, user_is_postpaid=True, new_money_cent=money, description="Set money manually via Admin UI")
t = text("UPDATE users_postpaid SET money = :money WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": user_id, "money": money})
@@ -166,7 +250,7 @@ def set_postpaid_user_money(user_id: int, money: float):
connection.commit()
return result.rowcount
def drink_postpaid_user(user_id: int):
def drink_postpaid_user(user_id: int, drink_type: str = ""):
"""
Deducts 100 units from the specified postpaid user's balance and records a drink entry.
Args:
@@ -182,6 +266,13 @@ def drink_postpaid_user(user_id: int):
raise HTTPException(status_code=403, detail="User not activated")
prev_money = get_postpaid_user(user_id)["money"]
_log_transaction(
user_id=user_id,
user_is_postpaid=True,
previous_money_cent=prev_money,
delta_money_cent=-DRINK_COST,
description="Drink button pressed"
)
t = text("UPDATE users_postpaid SET money = :money, last_drink = CURRENT_TIMESTAMP WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": user_id, "money": prev_money - DRINK_COST})
@@ -189,9 +280,14 @@ def drink_postpaid_user(user_id: int):
raise HTTPException(status_code=404, detail="User not found")
connection.commit()
t_without_drink_type = text("INSERT INTO drinks (postpaid_user_id, timestamp) VALUES (:postpaid_user_id, CURRENT_TIMESTAMP)")
t_with_drink_type = text("INSERT INTO drinks (postpaid_user_id, timestamp, drink_type) VALUES (:postpaid_user_id, CURRENT_TIMESTAMP, :drink_type)")
with engine.connect() as connection:
t = text("INSERT INTO drinks (postpaid_user_id, timestamp) VALUES (:postpaid_user_id, CURRENT_TIMESTAMP)")
result = connection.execute(t, {"postpaid_user_id": user_id})
if not drink_type:
result = connection.execute(t_without_drink_type, {"postpaid_user_id": user_id})
else:
result = connection.execute(t_with_drink_type, {"postpaid_user_id": user_id, "drink_type": drink_type})
if result.rowcount == 0:
raise HTTPException(status_code=500, detail="Failed to create drink entry")
connection.commit()
@@ -284,6 +380,13 @@ def drink_prepaid_user(user_db_id: int):
prev_money = user_dict["money"]
if prev_money < DRINK_COST:
raise HTTPException(status_code=403, detail="Not enough money")
_log_transaction(
user_id=user_db_id,
user_is_postpaid=False,
previous_money_cent=prev_money,
delta_money_cent=-DRINK_COST,
description="Drink button pressed"
)
t = text("UPDATE users_prepaid SET money = :money, last_drink = CURRENT_TIMESTAMP WHERE id = :id")
with engine.connect() as connection:
result = connection.execute(t, {"id": user_db_id, "money": prev_money - DRINK_COST})
@@ -312,6 +415,12 @@ def toggle_activate_prepaid_user(user_id: int):
def set_prepaid_user_money(user_id: int, money: int, postpaid_user_id: int):
t1 = text("UPDATE users_prepaid SET money = :money WHERE id = :id")
t2 = text("UPDATE users_prepaid SET postpaid_user_id = :postpaid_user_id WHERE id = :id")
_log_transaction(
user_id=user_id,
user_is_postpaid=False,
new_money_cent=money,
description="Set money manually via Admin UI"
)
with engine.connect() as connection:
result = connection.execute(t1, {"id": user_id, "money": money})
if result.rowcount == 0:

18
main.py
View File

@@ -1,7 +1,10 @@
import random
from fastapi import FastAPI, Request, Form, HTTPException
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.responses import JSONResponse
from starlette.middleware.sessions import SessionMiddleware
import uvicorn
@@ -143,7 +146,7 @@ def set_money_postpaid(request: Request, username = Form(...), money: float = Fo
return RedirectResponse(url="/", status_code=303)
@app.post("/drink")
def drink(request: Request):
async def drink(request: Request):
"""
Handles a drink purchase request for a user.
Checks if the user is authenticated and belongs to the admin group. If not, raises a 403 error.
@@ -167,7 +170,11 @@ def drink(request: Request):
if not user_db_id:
raise HTTPException(status_code=404, detail="User not found")
drink_postpaid_user(user_db_id)
form = await request.form()
getraenk = str(form.get("getraenk"))
print(f"User {user_authentik["preferred_username"]} requested drink: {getraenk}")
drink_postpaid_user(user_db_id, getraenk)
return RedirectResponse(url="/", status_code=303)
@app.post("/payup")
@@ -286,7 +293,6 @@ def add_money_prepaid_user(request: Request, username: str = Form(...), money: f
@app.post("/del_prepaid_user")
def delete_prepaid_user(request: Request, username: str = Form(...)):
# check if user is in ADMIN_GROUP
user_auth = request.session.get("user_authentik")
if not user_auth or ADMIN_GROUP not in user_auth["groups"]:
@@ -303,5 +309,11 @@ def delete_prepaid_user(request: Request, username: str = Form(...)):
return RedirectResponse(url="/", status_code=303)
@app.get("/popup_getraenke")
async def popup_getraenke():
alle_getraenke = ["Wasser", "Cola", "Bier", "Mate", "Saft", "Tee", "Kaffee", "Limo"]
return JSONResponse(content={"getraenke": random.sample(alle_getraenke, 4)})
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -107,4 +107,64 @@ content %}
ID {{ db_user.postpaid_user_id }}
</div>
{% endif %}
<div id="popup" style="display:none; position:fixed; top:25%; left:50%; transform:translate(-50%, -25%);
background:#fff; border:2px solid #00618F; border-radius:12px; padding:1em; box-shadow:0 0 20px rgba(0,0,0,0.3); z-index:1000; max-width:90%; text-align:center;">
<h2>Wähle dein Getränk oder warte bitte</h2>
<div id="popup-getraenke" style="margin: 1em 0;"></div>
<div id="progress-container" style="height:10px; background:#ccc; border-radius:5px; overflow:hidden;">
<div id="progress-bar" style="height:10px; background:#00618F; width:100%; transition: width 0.1s linear;"></div>
</div>
</div>
<script>
async function showDrinkPopup() {
const response = await fetch("/popup_getraenke");
const data = await response.json();
const container = document.getElementById("popup-getraenke");
container.innerHTML = "";
data.getraenke.forEach(name => {
const btn = document.createElement("button");
btn.textContent = name;
btn.style.margin = "0.5em";
btn.style.padding = "0.5em 1em";
btn.style.borderRadius = "6px";
btn.style.border = "1px solid #00618F";
btn.style.background = "#e0f4ff";
btn.style.cursor = "pointer";
btn.onclick = () => {
document.getElementById("getraenk-auswahl").value = name;
document.getElementById("popup").style.display = "none";
document.getElementById("drink-form").submit();
};
container.appendChild(btn);
});
document.getElementById("popup").style.display = "block";
let dauer = 3; // Sekunden
let verbleibend = dauer;
const bar = document.getElementById("progress-bar");
bar.style.width = "100%";
const interval = setInterval(() => {
verbleibend -= 0.1;
bar.style.width = (verbleibend / dauer * 100) + "%";
if (verbleibend <= 0) {
clearInterval(interval);
const auswahlInput = document.getElementById("getraenk-auswahl");
if (auswahlInput) {
auswahlInput.value = "";
}
document.getElementById("popup").style.display = "none";
const drinkForm = document.getElementById("drink-form");
if (drinkForm) {
drinkForm.submit();
}
}
}, 100);
console.log("Popup angezeigt und Fortschrittsbalken gestartet.");
}
</script>
{% endblock %}