Add transaction table and prepare drink popup
This commit is contained in:
121
db/models.py
121
db/models.py
@@ -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
18
main.py
@@ -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)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user