diff --git a/src/allmende_payment_system/api/shop.py b/src/allmende_payment_system/api/shop.py
index db05c3c..de5b6a0 100644
--- a/src/allmende_payment_system/api/shop.py
+++ b/src/allmende_payment_system/api/shop.py
@@ -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
):
diff --git a/src/allmende_payment_system/models.py b/src/allmende_payment_system/models.py
index 96d6ed0..a856baa 100644
--- a/src/allmende_payment_system/models.py
+++ b/src/allmende_payment_system/models.py
@@ -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",
diff --git a/src/allmende_payment_system/templates/area.html.jinja b/src/allmende_payment_system/templates/area.html.jinja
index 94b2938..6f2d47a 100644
--- a/src/allmende_payment_system/templates/area.html.jinja
+++ b/src/allmende_payment_system/templates/area.html.jinja
@@ -67,14 +67,15 @@
- Menge
+ Menge{% if product.unit_of_measure != 'piece' %} (in {{product.unit_of_measure}}){% endif %}
diff --git a/src/allmende_payment_system/templates/index.html.jinja b/src/allmende_payment_system/templates/index.html.jinja
index 88b2cd6..f079ef0 100644
--- a/src/allmende_payment_system/templates/index.html.jinja
+++ b/src/allmende_payment_system/templates/index.html.jinja
@@ -35,7 +35,7 @@
-
Latest Transactions
+
Letzte Buchungen
View All
diff --git a/src/allmende_payment_system/templates/order.html.jinja b/src/allmende_payment_system/templates/order.html.jinja
index be2fe5f..1f2422c 100644
--- a/src/allmende_payment_system/templates/order.html.jinja
+++ b/src/allmende_payment_system/templates/order.html.jinja
@@ -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 %}
@@ -35,18 +34,26 @@
{% if is_cart %}
-
{% else %}
- {{ item.quantity | format_number }}
+ {{ item.quantity | format_number }}{% if item.product.unit_of_measure != 'piece' %} {{ item.product.unit_of_measure }}{% endif %}
{% endif %}
{{ item.product.price | format_number }} €
{{ item.total_amount | format_number }} €
- {% if is_cart %}Entfernen {% endif %}
+ {% if is_cart %}Entfernen {% endif %}
{% endfor %}
@@ -64,7 +71,6 @@
{% if is_cart %}
diff --git a/test/test_shop.py b/test/test_shop.py
index 8fae41b..d6eeaff 100644
--- a/test/test_shop.py
+++ b/test/test_shop.py
@@ -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):