View order

This commit is contained in:
2025-12-13 12:25:00 +01:00
parent fd544fcebc
commit 5d2dfe37c1
5 changed files with 101 additions and 34 deletions

View File

@@ -1,12 +1,12 @@
from decimal import Decimal from decimal import Decimal
from fastapi import APIRouter, Request from fastapi import APIRouter, HTTPException, Request
from sqlalchemy import select from sqlalchemy import select
from starlette import status from starlette import status
from starlette.responses import RedirectResponse from starlette.responses import RedirectResponse
from allmende_payment_system.api.dependencies import SessionDep, UserDep from allmende_payment_system.api.dependencies import SessionDep, UserDep
from allmende_payment_system.models import Area, OrderItem, Product from allmende_payment_system.models import Area, Order, OrderItem, Product
from allmende_payment_system.tools import get_jinja_renderer from allmende_payment_system.tools import get_jinja_renderer
shop_router = APIRouter() shop_router = APIRouter()
@@ -28,8 +28,8 @@ async def get_shop(request: Request, session: SessionDep):
async def get_cart(request: Request, session: SessionDep, user: UserDep): async def get_cart(request: Request, session: SessionDep, user: UserDep):
return templates.TemplateResponse( return templates.TemplateResponse(
"cart.html.jinja", "order.html.jinja",
context={"request": request, "user": user}, context={"request": request, "order": user.shopping_cart, "is_cart": True},
) )
@@ -76,3 +76,22 @@ async def add_to_cart(request: Request, session: SessionDep, user: UserDep):
return RedirectResponse( return RedirectResponse(
url=f"/shop/area/{form_data['area_id']}", status_code=status.HTTP_302_FOUND url=f"/shop/area/{form_data['area_id']}", status_code=status.HTTP_302_FOUND
) )
@shop_router.get("/shop/order/{order_id}")
async def add_to_cart(
request: Request, session: SessionDep, user: UserDep, order_id: int
):
query = select(Order).where(Order.id == order_id)
order = session.scalars(query).one()
if user.id != order.user_id:
raise HTTPException(
status_code=403, detail=f"User not authorized to view this order."
)
return templates.TemplateResponse(
"order.html.jinja",
context={"request": request, "order": order, "is_cart": False},
)

View File

@@ -45,10 +45,15 @@
{% for transaction in transactions[:10] %} {% for transaction in transactions[:10] %}
<div class="list-group-item d-flex justify-content-between align-items-start py-3"> <div class="list-group-item d-flex justify-content-between align-items-start py-3">
<div class="flex-grow-1"> <div class="flex-grow-1">
<div class="fw-semibold">{{ transaction.type|transaction_type_de }}</div> <div class="fw-semibold d-inline">{{ transaction.type|transaction_type_de }}</div>
<small class="text-muted"> <small class="text-muted d-inline ms-2">
{{ transaction.timestamp }} {{ transaction.timestamp | timestamp_de }}
</small> </small>
{% if transaction.type == "order" %}
<div class="mt-2">
<a href="/shop/order/{{ transaction.order_id }}" class="btn btn-sm btn-outline-primary">Einkauf ansehen</a>
</div>
{% endif %}
</div> </div>
<div class="text-end ms-3"> <div class="text-end ms-3">
<span class="fs-5 fw-bold {% if transaction.total_amount < 0 %}text-danger{% else %}text-success{% endif %}"> <span class="fs-5 fw-bold {% if transaction.total_amount < 0 %}text-danger{% else %}text-success{% endif %}">

View File

@@ -1,11 +1,11 @@
{% extends "base.html.jinja" %} {% extends "base.html.jinja" %}
{% block content %} {% block content %}
<div class="mb-4"> <div class="mb-4">
<h2 class="h4 mb-3">Warenkorb</h2> <h2 class="h4 mb-3">{% if is_cart %}Warenkorb{% else %}Einkauf #{{ order.id }}{% endif %}</h2>
<p class="text-muted">Überprüfe deine Artikel und fahre zur Kasse fort.</p> {% if not is_cart %}<p class="text-muted">Einkauf abgeschickt: {{ order.transaction.timestamp | timestamp_de }}</p>{% endif %}
</div> </div>
{% set items = user.shopping_cart.items %} {% set items = order.items %}
{% if items|length == 0 %} {% if items|length == 0 %}
<div class="alert alert-info">Dein Warenkorb ist leer. <a href="/shop" class="alert-link">Weiter einkaufen</a>.</div> <div class="alert alert-info">Dein Warenkorb ist leer. <a href="/shop" class="alert-link">Weiter einkaufen</a>.</div>
@@ -34,15 +34,19 @@
<div class="text-muted small">{{ item.description or '' }}</div> <div class="text-muted small">{{ item.description or '' }}</div>
</td> </td>
<td class="text-center" style="width:180px;"> <td class="text-center" style="width:180px;">
{% if is_cart %}
<form method="post" action="/cart/update/{{ item.id }}" class="d-flex align-items-center justify-content-center"> <form method="post" action="/cart/update/{{ item.id }}" class="d-flex align-items-center justify-content-center">
<input type="number" name="quantity" value="{{ qty }}" min="1" step="0.01" class="form-control form-control-sm me-2" style="width:80px;"> <input type="number" name="quantity" value="{{ qty }}" min="1" step="0.01" class="form-control form-control-sm me-2" style="width:80px;">
<button class="btn btn-sm btn-outline-secondary" type="submit">Aktualisieren</button> <button class="btn btn-sm btn-outline-secondary" type="submit">Aktualisieren</button>
</form> </form>
{% else %}
{{ item.quantity | format_number }}
{% endif %}
</td> </td>
<td class="text-end">{{ item.product.price | format_number }} €</td> <td class="text-end">{{ item.product.price | format_number }} €</td>
<td class="text-end">{{ item.total_amount | format_number }} €</td> <td class="text-end">{{ item.total_amount | format_number }} €</td>
<td class="text-end"> <td class="text-end">
<a href="/cart/remove/{{ item.id }}" class="btn btn-sm btn-outline-danger">Entfernen</a> {% if is_cart %}<a href="/cart/remove/{{ item.id }}" class="btn btn-sm btn-outline-danger">Entfernen</a>{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
@@ -50,13 +54,14 @@
<tfoot> <tfoot>
<tr> <tr>
<td colspan="3" class="text-end fw-semibold">Gesamtsumme</td> <td colspan="3" class="text-end fw-semibold">Gesamtsumme</td>
<td class="text-end fw-bold">{{ user.shopping_cart.total_amount | format_number }} €</td> <td class="text-end fw-bold">{{ order.total_amount | format_number }} €</td>
<td></td> <td></td>
</tr> </tr>
</tfoot> </tfoot>
</table> </table>
</div> </div>
{% if is_cart %}
<div class="d-flex justify-content-between align-items-center mt-3"> <div class="d-flex justify-content-between align-items-center mt-3">
<div> <div>
<a href="/cart/clear" class="btn btn-outline-danger me-2">Warenkorb leeren</a> <a href="/cart/clear" class="btn btn-outline-danger me-2">Warenkorb leeren</a>
@@ -64,6 +69,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -15,7 +15,7 @@ UNITS_OF_MEASURE = {"piece": "Stück"}
def format_number(value: float): def format_number(value: float):
try: try:
return f"{value:n}" return f"{value:.2f}".replace(".", ",")
except TypeError: except TypeError:
return value return value
@@ -25,4 +25,5 @@ def get_jinja_renderer() -> Jinja2Templates:
renderer.env.filters["transaction_type_de"] = lambda x: TRANSACTION_TYPE_DE[x] renderer.env.filters["transaction_type_de"] = lambda x: TRANSACTION_TYPE_DE[x]
renderer.env.filters["units_of_measure_de"] = lambda x: UNITS_OF_MEASURE.get(x, x) renderer.env.filters["units_of_measure_de"] = lambda x: UNITS_OF_MEASURE.get(x, x)
renderer.env.filters["format_number"] = format_number renderer.env.filters["format_number"] = format_number
renderer.env.filters["timestamp_de"] = lambda x: x.strftime("%d.%m.%Y %H:%M")
return renderer return renderer

View File

@@ -1,5 +1,7 @@
from decimal import Decimal from decimal import Decimal
from unittest import mock
import pytest
from fake_data import fake from fake_data import fake
from starlette.testclient import TestClient from starlette.testclient import TestClient
@@ -29,14 +31,33 @@ def create_user_with_account(test_db, username: str, balance: float | None = Non
return user return user
def test_add_item_to_cart(client: TestClient, test_db): def add_finalized_order_to_user(test_db, user, product) -> Order:
order = Order(user=user)
total_amount = product.price
order.items.append(
OrderItem(product=product, quantity=1, total_amount=total_amount)
)
order.transaction = Transaction(total_amount=total_amount, type="order")
order.transaction.account = user.accounts[0]
test_db.add(order)
test_db.flush()
return order
@pytest.fixture
def product(test_db):
area = Area(**fake.area()) area = Area(**fake.area())
test_db.add(area) test_db.add(area)
product = Product(**fake.product()) product = Product(**fake.product())
product.area = area product.area = area
test_db.add(product) test_db.add(product)
test_db.flush() test_db.flush()
form_data = {"product_id": product.id, "quantity": 2, "area_id": area.id} return product
def test_add_item_to_cart(client: TestClient, test_db, product):
form_data = {"product_id": product.id, "quantity": 2, "area_id": product.area.id}
response = client.post( response = client.post(
"/shop/cart/add", "/shop/cart/add",
@@ -47,13 +68,7 @@ def test_add_item_to_cart(client: TestClient, test_db):
assert response.status_code == 302 assert response.status_code == 302
def test_finalize_order(client: TestClient, test_db): def test_finalize_order(client: TestClient, test_db, product):
area = Area(**fake.area())
test_db.add(area)
product = Product(**fake.product())
product.area = area
test_db.add(product)
test_db.flush()
user = create_user_with_account(test_db, "test", balance=100.0) user = create_user_with_account(test_db, "test", balance=100.0)
@@ -69,3 +84,24 @@ def test_finalize_order(client: TestClient, test_db):
assert len(user.orders) == 2 # shopping cart + finalized order assert len(user.orders) == 2 # shopping cart + finalized order
assert user.accounts[0].balance == Decimal(100.0) - (product.price * 2) assert user.accounts[0].balance == Decimal(100.0) - (product.price * 2)
def test_view_order(client: TestClient, test_db, product):
user = create_user_with_account(test_db, "test")
order = add_finalized_order_to_user(test_db, user, product)
response = client.get(f"/shop/order/{order.id}")
assert response.status_code == 200
assert f"Einkauf #{order.id}" in response.text
assert product.name in response.text
def test_view_order_wrong_user(client: TestClient, test_db, product):
user = create_user_with_account(test_db, "test")
order = add_finalized_order_to_user(test_db, user, product)
with mock.patch.dict("os.environ", {"APS_username": "other_user"}):
response = client.get(f"/shop/order/{order.id}")
assert response.status_code == 403