Modify cart items

This commit is contained in:
2025-12-13 14:30:14 +01:00
parent 5d2dfe37c1
commit d1fc874c35
6 changed files with 137 additions and 11 deletions

View File

@@ -78,8 +78,43 @@ async def add_to_cart(request: Request, session: SessionDep, user: UserDep):
)
@shop_router.get("/shop/cart/remove/{item_id}")
async def remove_from_cart(
request: Request, session: SessionDep, user: UserDep, item_id: int
):
cart = user.shopping_cart
for item in cart.items:
if item.id == item_id:
item.order = None
session.delete(item)
break
else:
raise HTTPException(status_code=404, detail="Item not found in cart.")
return RedirectResponse(url=f"/shop/cart", status_code=status.HTTP_302_FOUND)
@shop_router.post("/shop/cart/update/{item_id}")
async def update_cart_item(
request: Request, session: SessionDep, user: UserDep, item_id: int
):
form_data = await request.form()
cart = user.shopping_cart
for item in cart.items:
if item.id == item_id:
item.update_quantity(Decimal(form_data["quantity"]))
break
else:
raise HTTPException(status_code=404, detail="Item not found in cart.")
return RedirectResponse(url=f"/shop/cart", status_code=status.HTTP_302_FOUND)
@shop_router.get("/shop/order/{order_id}")
async def add_to_cart(
async def get_order_details(
request: Request, session: SessionDep, user: UserDep, order_id: int
):

View File

@@ -94,6 +94,8 @@ class Product(Base):
price: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
unit_of_measure: Mapped[UnitsOfMeasure] = mapped_column(nullable=False)
allow_fractional: Mapped[bool] = mapped_column(nullable=False, default=True)
# TODO: limit this to actually used vat rates?
vat_rate: Mapped[decimal.Decimal] = mapped_column(Numeric(10, 2))
@@ -162,6 +164,13 @@ class OrderItem(Base):
Numeric(10, 2), nullable=False
)
def update_quantity(self, new_quantity: decimal.Decimal):
if new_quantity <= 0:
raise ValueError("Quantity must be positive.")
self.quantity = new_quantity
self.total_amount = self.product.price * new_quantity
TransactionTypes = typing.Literal[
"order",

View File

@@ -67,14 +67,15 @@
<input type="hidden" name="product_id" value="{{ product.id }}">
<input type="hidden" name="area_id" value="{{ area.id }}">
<div class="mb-3">
<label for="quantity{{ product.id }}" class="form-label">Menge</label>
<label for="quantity{{ product.id }}" class="form-label">Menge{% if product.unit_of_measure != 'piece' %} (in {{product.unit_of_measure}}){% endif %}</label>
<input type="number"
class="form-control"
id="quantity{{ product.id }}"
name="quantity"
value="1"
min="1"
{% if product.allow_fractional %}min="0.01"{% else %}min="1"{% endif %}
max="999"
{% if product.allow_fractional %}step="0.01"{% else %}step="1"{% endif %}
oninput="updateTotal{{ product.id }}(this.value)"
style="max-width: 150px;">
</div>

View File

@@ -35,7 +35,7 @@
<!-- Latest Transactions Section -->
<div class="transactions-section">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="h3 mb-0">Latest Transactions</h2>
<h2 class="h3 mb-0">Letzte Buchungen</h2>
<a href="#" class="btn btn-outline-primary btn-sm">View All</a>
</div>

View File

@@ -25,8 +25,7 @@
{% set total = namespace(value=0) %}
{% for item in items %}
{% set price = item.price if item.price is defined else (item.unit_price if item.unit_price is defined else 0) %}
{% set qty = item.quantity if item.quantity is defined else (item.qty if item.qty is defined else 1) %}
{% set subtotal = price * qty %}
{% set subtotal = price * item.quantity %}
{% set total.value = total.value + subtotal %}
<tr>
<td>
@@ -35,18 +34,26 @@
</td>
<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">
<input type="number" name="quantity" value="{{ qty }}" min="1" step="0.01" class="form-control form-control-sm me-2" style="width:80px;">
<form method="post" action="/shop/cart/update/{{ item.id }}" class="d-flex align-items-center justify-content-center">
<input
type="number"
name="quantity"
value="{% if item.product.allow_fractional %}{{ item.quantity | format_number }}{% else %}{{ item.quantity | int }}{% endif %}"
{% if item.product.allow_fractional %}min="0.01" step="0.01"{% else %}min="1" step="1"{% endif %}
class="form-control form-control-sm me-2"
style="width:80px;"
required>
{% if item.product.unit_of_measure != 'piece' %}<span class="text-muted small ms-1 me-2">{{ item.product.unit_of_measure }}</span>{% endif %}
<button class="btn btn-sm btn-outline-secondary" type="submit">Aktualisieren</button>
</form>
{% else %}
{{ item.quantity | format_number }}
{{ item.quantity | format_number }}{% if item.product.unit_of_measure != 'piece' %} {{ item.product.unit_of_measure }}{% endif %}
{% endif %}
</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">
{% if is_cart %}<a href="/cart/remove/{{ item.id }}" class="btn btn-sm btn-outline-danger">Entfernen</a>{% endif %}
{% if is_cart %}<a href="/shop/cart/remove/{{ item.id }}" class="btn btn-sm btn-outline-danger">Entfernen</a>{% endif %}
</td>
</tr>
{% endfor %}
@@ -64,7 +71,6 @@
{% if is_cart %}
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<a href="/cart/clear" class="btn btn-outline-danger me-2">Warenkorb leeren</a>
<a href="/shop/finalize_order" class="btn btn-primary">Jetzt Buchen</a>
</div>
</div>

View File

@@ -3,6 +3,7 @@ from unittest import mock
import pytest
from fake_data import fake
from sqlalchemy import select
from starlette.testclient import TestClient
from allmende_payment_system.database import ensure_user
@@ -67,6 +68,80 @@ def test_add_item_to_cart(client: TestClient, test_db, product):
)
assert response.status_code == 302
cart = test_db.scalar(select(Order))
assert len(cart.items) == 1
def test_edit_item_in_cart(client: TestClient, test_db, product):
form_data = {"product_id": product.id, "quantity": 2, "area_id": product.area.id}
response = client.post(
"/shop/cart/add",
data=form_data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
follow_redirects=False,
)
assert response.status_code == 302
form_data = {"quantity": 3}
response = client.post(
f"/shop/cart/update/{test_db.scalar(select(OrderItem)).id}",
data=form_data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
follow_redirects=False,
)
assert response.status_code == 302
cart = test_db.scalar(select(Order))
assert len(cart.items) == 1
assert cart.items[0].quantity == 3
assert cart.items[0].total_amount == product.price * 3
def test_remove_item_from_cart(client: TestClient, test_db, product):
user = create_user_with_account(test_db, "test")
user.shopping_cart.items.append(
OrderItem(product=product, quantity=2, total_amount=product.price * 2)
)
test_db.flush()
cart = test_db.scalar(select(Order))
assert len(cart.items) == 1
response = client.get(
f"/shop/cart/remove/{user.shopping_cart.items[0].id}", follow_redirects=False
)
assert response.status_code == 302
cart = test_db.scalar(select(Order))
assert len(cart.items) == 0
assert len(test_db.scalars(select(OrderItem)).all()) == 0
def test_remove_item_from_cart_wrong_user(client: TestClient, test_db, product):
user = create_user_with_account(test_db, "test")
user.shopping_cart.items.append(
OrderItem(product=product, quantity=2, total_amount=product.price * 2)
)
test_db.flush()
cart = test_db.scalar(select(Order))
assert len(cart.items) == 1
id_ = user.shopping_cart.items[0].id
with mock.patch.dict("os.environ", {"APS_username": "other_user"}):
response = client.get(f"/shop/cart/remove/{id_}", follow_redirects=False)
assert response.status_code == 404
cart = test_db.scalar(select(Order))
assert len(cart.items) == 1
def test_finalize_order(client: TestClient, test_db, product):