Fix egg sale form: remove duplicate route, change price to euros
All checks were successful
Deploy / deploy (push) Successful in 2m50s
All checks were successful
Deploy / deploy (push) Successful in 2m50s
The egg sale form had two issues: - Duplicate POST /actions/product-sold route in products.py was overwriting the eggs.py handler, causing incomplete page responses (no tabs, no recent sales list) - Price input used cents while feed purchase uses euros, inconsistent UX Changes: - Remove duplicate handler from products.py (keep only redirect) - Change sell form price input from cents to euros (consistent with feed) - Parse euros in handler, convert to cents for storage - Add TestEggSale class with 4 tests for the fixed behavior Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -547,7 +547,7 @@ async def product_sold(request: Request, session):
|
|||||||
# Extract form data
|
# Extract form data
|
||||||
product_code = form.get("product_code", "")
|
product_code = form.get("product_code", "")
|
||||||
quantity_str = form.get("quantity", "0")
|
quantity_str = form.get("quantity", "0")
|
||||||
total_price_str = form.get("total_price_cents", "0")
|
total_price_str = form.get("total_price_euros", "0")
|
||||||
buyer = form.get("buyer") or None
|
buyer = form.get("buyer") or None
|
||||||
notes = form.get("notes") or None
|
notes = form.get("notes") or None
|
||||||
nonce = form.get("nonce")
|
nonce = form.get("nonce")
|
||||||
@@ -566,7 +566,7 @@ async def product_sold(request: Request, session):
|
|||||||
None,
|
None,
|
||||||
"Please select a product",
|
"Please select a product",
|
||||||
quantity=quantity_str,
|
quantity=quantity_str,
|
||||||
total_price_cents=total_price_str,
|
total_price_euros=total_price_str,
|
||||||
buyer=buyer,
|
buyer=buyer,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
@@ -583,7 +583,7 @@ async def product_sold(request: Request, session):
|
|||||||
product_code,
|
product_code,
|
||||||
"Quantity must be a number",
|
"Quantity must be a number",
|
||||||
quantity=quantity_str,
|
quantity=quantity_str,
|
||||||
total_price_cents=total_price_str,
|
total_price_euros=total_price_str,
|
||||||
buyer=buyer,
|
buyer=buyer,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
@@ -597,14 +597,15 @@ async def product_sold(request: Request, session):
|
|||||||
product_code,
|
product_code,
|
||||||
"Quantity must be at least 1",
|
"Quantity must be at least 1",
|
||||||
quantity=quantity_str,
|
quantity=quantity_str,
|
||||||
total_price_cents=total_price_str,
|
total_price_euros=total_price_str,
|
||||||
buyer=buyer,
|
buyer=buyer,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Validate total_price_cents
|
# Validate total_price_euros and convert to cents
|
||||||
try:
|
try:
|
||||||
total_price_cents = int(total_price_str)
|
total_price_euros = float(total_price_str)
|
||||||
|
total_price_cents = int(round(total_price_euros * 100))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return _render_sell_error(
|
return _render_sell_error(
|
||||||
request,
|
request,
|
||||||
@@ -614,7 +615,7 @@ async def product_sold(request: Request, session):
|
|||||||
product_code,
|
product_code,
|
||||||
"Total price must be a number",
|
"Total price must be a number",
|
||||||
quantity=quantity_str,
|
quantity=quantity_str,
|
||||||
total_price_cents=total_price_str,
|
total_price_euros=total_price_str,
|
||||||
buyer=buyer,
|
buyer=buyer,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
@@ -628,7 +629,7 @@ async def product_sold(request: Request, session):
|
|||||||
product_code,
|
product_code,
|
||||||
"Total price cannot be negative",
|
"Total price cannot be negative",
|
||||||
quantity=quantity_str,
|
quantity=quantity_str,
|
||||||
total_price_cents=total_price_str,
|
total_price_euros=total_price_str,
|
||||||
buyer=buyer,
|
buyer=buyer,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
@@ -671,7 +672,7 @@ async def product_sold(request: Request, session):
|
|||||||
product_code,
|
product_code,
|
||||||
str(e),
|
str(e),
|
||||||
quantity=quantity_str,
|
quantity=quantity_str,
|
||||||
total_price_cents=total_price_str,
|
total_price_euros=total_price_str,
|
||||||
buyer=buyer,
|
buyer=buyer,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
@@ -763,7 +764,7 @@ def _render_sell_error(
|
|||||||
selected_product_code,
|
selected_product_code,
|
||||||
error_message,
|
error_message,
|
||||||
quantity: str | None = None,
|
quantity: str | None = None,
|
||||||
total_price_cents: str | None = None,
|
total_price_euros: str | None = None,
|
||||||
buyer: str | None = None,
|
buyer: str | None = None,
|
||||||
notes: str | None = None,
|
notes: str | None = None,
|
||||||
):
|
):
|
||||||
@@ -777,7 +778,7 @@ def _render_sell_error(
|
|||||||
selected_product_code: Currently selected product code.
|
selected_product_code: Currently selected product code.
|
||||||
error_message: Error message to display.
|
error_message: Error message to display.
|
||||||
quantity: Quantity value to preserve.
|
quantity: Quantity value to preserve.
|
||||||
total_price_cents: Total price value to preserve.
|
total_price_euros: Total price value to preserve.
|
||||||
buyer: Buyer value to preserve.
|
buyer: Buyer value to preserve.
|
||||||
notes: Notes value to preserve.
|
notes: Notes value to preserve.
|
||||||
|
|
||||||
@@ -798,7 +799,7 @@ def _render_sell_error(
|
|||||||
harvest_action=product_collected,
|
harvest_action=product_collected,
|
||||||
sell_action=product_sold,
|
sell_action=product_sold,
|
||||||
sell_quantity=quantity,
|
sell_quantity=quantity,
|
||||||
sell_total_price_cents=total_price_cents,
|
sell_total_price_euros=total_price_euros,
|
||||||
sell_buyer=buyer,
|
sell_buyer=buyer,
|
||||||
sell_notes=notes,
|
sell_notes=notes,
|
||||||
**display_data,
|
**display_data,
|
||||||
|
|||||||
@@ -1,47 +1,19 @@
|
|||||||
# ABOUTME: Routes for Product Sold functionality.
|
# ABOUTME: Routes for Product Sold functionality.
|
||||||
# ABOUTME: Handles GET /sell form and POST /actions/product-sold.
|
# ABOUTME: Redirects GET /sell to Eggs page Sell tab. POST handled by eggs.py.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
from fasthtml.common import APIRouter
|
||||||
import time
|
|
||||||
|
|
||||||
from fasthtml.common import APIRouter, to_xml
|
|
||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
from starlette.responses import HTMLResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
|
||||||
from animaltrack.events.payloads import ProductSoldPayload
|
|
||||||
from animaltrack.events.store import EventStore
|
|
||||||
from animaltrack.projections import EventLogProjection, ProjectionRegistry
|
|
||||||
from animaltrack.projections.products import ProductsProjection
|
|
||||||
from animaltrack.repositories.products import ProductRepository
|
|
||||||
from animaltrack.services.products import ProductService, ValidationError
|
|
||||||
from animaltrack.web.templates import render_page
|
|
||||||
from animaltrack.web.templates.products import product_sold_form
|
|
||||||
|
|
||||||
# APIRouter for multi-file route organization
|
# APIRouter for multi-file route organization
|
||||||
ar = APIRouter()
|
ar = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
def _get_sellable_products(db):
|
|
||||||
"""Get list of active, sellable products.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database connection.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of sellable Product objects.
|
|
||||||
"""
|
|
||||||
repo = ProductRepository(db)
|
|
||||||
all_products = repo.list_all()
|
|
||||||
return [p for p in all_products if p.active and p.sellable]
|
|
||||||
|
|
||||||
|
|
||||||
@ar("/sell")
|
@ar("/sell")
|
||||||
def sell_index(request: Request):
|
def sell_index(request: Request):
|
||||||
"""GET /sell - Redirect to Eggs page Sell tab."""
|
"""GET /sell - Redirect to Eggs page Sell tab."""
|
||||||
from starlette.responses import RedirectResponse
|
|
||||||
|
|
||||||
# Preserve product_code if provided
|
# Preserve product_code if provided
|
||||||
product_code = request.query_params.get("product_code")
|
product_code = request.query_params.get("product_code")
|
||||||
redirect_url = "/?tab=sell"
|
redirect_url = "/?tab=sell"
|
||||||
@@ -49,130 +21,3 @@ def sell_index(request: Request):
|
|||||||
redirect_url = f"/?tab=sell&product_code={product_code}"
|
redirect_url = f"/?tab=sell&product_code={product_code}"
|
||||||
|
|
||||||
return RedirectResponse(url=redirect_url, status_code=302)
|
return RedirectResponse(url=redirect_url, status_code=302)
|
||||||
|
|
||||||
|
|
||||||
@ar("/actions/product-sold", methods=["POST"])
|
|
||||||
async def product_sold(request: Request):
|
|
||||||
"""POST /actions/product-sold - Record product sale."""
|
|
||||||
db = request.app.state.db
|
|
||||||
form = await request.form()
|
|
||||||
|
|
||||||
# Extract form data
|
|
||||||
product_code = form.get("product_code", "")
|
|
||||||
quantity_str = form.get("quantity", "0")
|
|
||||||
total_price_str = form.get("total_price_cents", "0")
|
|
||||||
buyer = form.get("buyer") or None
|
|
||||||
notes = form.get("notes") or None
|
|
||||||
nonce = form.get("nonce")
|
|
||||||
|
|
||||||
# Get products for potential re-render
|
|
||||||
products = _get_sellable_products(db)
|
|
||||||
|
|
||||||
# Validate product_code
|
|
||||||
if not product_code:
|
|
||||||
return _render_error_form(request, products, None, "Please select a product")
|
|
||||||
|
|
||||||
# Validate quantity
|
|
||||||
try:
|
|
||||||
quantity = int(quantity_str)
|
|
||||||
except ValueError:
|
|
||||||
return _render_error_form(request, products, product_code, "Quantity must be a number")
|
|
||||||
|
|
||||||
if quantity < 1:
|
|
||||||
return _render_error_form(request, products, product_code, "Quantity must be at least 1")
|
|
||||||
|
|
||||||
# Validate total_price_cents
|
|
||||||
try:
|
|
||||||
total_price_cents = int(total_price_str)
|
|
||||||
except ValueError:
|
|
||||||
return _render_error_form(request, products, product_code, "Total price must be a number")
|
|
||||||
|
|
||||||
if total_price_cents < 0:
|
|
||||||
return _render_error_form(request, products, product_code, "Total price cannot be negative")
|
|
||||||
|
|
||||||
# Get current timestamp
|
|
||||||
ts_utc = int(time.time() * 1000)
|
|
||||||
|
|
||||||
# Create product service
|
|
||||||
event_store = EventStore(db)
|
|
||||||
registry = ProjectionRegistry()
|
|
||||||
registry.register(ProductsProjection(db))
|
|
||||||
registry.register(EventLogProjection(db))
|
|
||||||
|
|
||||||
product_service = ProductService(db, event_store, registry)
|
|
||||||
|
|
||||||
# Create payload
|
|
||||||
payload = ProductSoldPayload(
|
|
||||||
product_code=product_code,
|
|
||||||
quantity=quantity,
|
|
||||||
total_price_cents=total_price_cents,
|
|
||||||
buyer=buyer,
|
|
||||||
notes=notes,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get actor from auth
|
|
||||||
auth = request.scope.get("auth")
|
|
||||||
actor = auth.username if auth else "unknown"
|
|
||||||
|
|
||||||
# Sell product
|
|
||||||
try:
|
|
||||||
product_service.sell_product(
|
|
||||||
payload=payload,
|
|
||||||
ts_utc=ts_utc,
|
|
||||||
actor=actor,
|
|
||||||
nonce=nonce,
|
|
||||||
route="/actions/product-sold",
|
|
||||||
)
|
|
||||||
except ValidationError as e:
|
|
||||||
return _render_error_form(request, products, product_code, str(e))
|
|
||||||
|
|
||||||
# Success: re-render form with product sticking, other fields cleared
|
|
||||||
response = HTMLResponse(
|
|
||||||
content=to_xml(
|
|
||||||
render_page(
|
|
||||||
request,
|
|
||||||
product_sold_form(
|
|
||||||
products, selected_product_code=product_code, action=product_sold
|
|
||||||
),
|
|
||||||
title="Sell - AnimalTrack",
|
|
||||||
active_nav=None,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add toast trigger header
|
|
||||||
response.headers["HX-Trigger"] = json.dumps(
|
|
||||||
{"showToast": {"message": f"Recorded sale of {quantity} {product_code}", "type": "success"}}
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
def _render_error_form(request, products, selected_product_code, error_message):
|
|
||||||
"""Render form with error message.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: The Starlette request object.
|
|
||||||
products: List of sellable products.
|
|
||||||
selected_product_code: Currently selected product code.
|
|
||||||
error_message: Error message to display.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
HTMLResponse with 422 status.
|
|
||||||
"""
|
|
||||||
return HTMLResponse(
|
|
||||||
content=to_xml(
|
|
||||||
render_page(
|
|
||||||
request,
|
|
||||||
product_sold_form(
|
|
||||||
products,
|
|
||||||
selected_product_code=selected_product_code,
|
|
||||||
error=error_message,
|
|
||||||
action=product_sold,
|
|
||||||
),
|
|
||||||
title="Sell - AnimalTrack",
|
|
||||||
active_nav=None,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
status_code=422,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def eggs_page(
|
|||||||
harvest_quantity: str | None = None,
|
harvest_quantity: str | None = None,
|
||||||
harvest_notes: str | None = None,
|
harvest_notes: str | None = None,
|
||||||
sell_quantity: str | None = None,
|
sell_quantity: str | None = None,
|
||||||
sell_total_price_cents: str | None = None,
|
sell_total_price_euros: str | None = None,
|
||||||
sell_buyer: str | None = None,
|
sell_buyer: str | None = None,
|
||||||
sell_notes: str | None = None,
|
sell_notes: str | None = None,
|
||||||
):
|
):
|
||||||
@@ -71,7 +71,7 @@ def eggs_page(
|
|||||||
harvest_quantity: Preserved quantity value on error.
|
harvest_quantity: Preserved quantity value on error.
|
||||||
harvest_notes: Preserved notes value on error.
|
harvest_notes: Preserved notes value on error.
|
||||||
sell_quantity: Preserved quantity value on error.
|
sell_quantity: Preserved quantity value on error.
|
||||||
sell_total_price_cents: Preserved total price value on error.
|
sell_total_price_euros: Preserved total price value on error.
|
||||||
sell_buyer: Preserved buyer value on error.
|
sell_buyer: Preserved buyer value on error.
|
||||||
sell_notes: Preserved notes value on error.
|
sell_notes: Preserved notes value on error.
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ def eggs_page(
|
|||||||
recent_events=sell_events,
|
recent_events=sell_events,
|
||||||
sales_stats=sales_stats,
|
sales_stats=sales_stats,
|
||||||
default_quantity=sell_quantity,
|
default_quantity=sell_quantity,
|
||||||
default_total_price_cents=sell_total_price_cents,
|
default_total_price_euros=sell_total_price_euros,
|
||||||
default_buyer=sell_buyer,
|
default_buyer=sell_buyer,
|
||||||
default_notes=sell_notes,
|
default_notes=sell_notes,
|
||||||
),
|
),
|
||||||
@@ -270,7 +270,7 @@ def sell_form(
|
|||||||
recent_events: list[tuple[Event, bool]] | None = None,
|
recent_events: list[tuple[Event, bool]] | None = None,
|
||||||
sales_stats: dict | None = None,
|
sales_stats: dict | None = None,
|
||||||
default_quantity: str | None = None,
|
default_quantity: str | None = None,
|
||||||
default_total_price_cents: str | None = None,
|
default_total_price_euros: str | None = None,
|
||||||
default_buyer: str | None = None,
|
default_buyer: str | None = None,
|
||||||
default_notes: str | None = None,
|
default_notes: str | None = None,
|
||||||
) -> Div:
|
) -> Div:
|
||||||
@@ -284,7 +284,7 @@ def sell_form(
|
|||||||
recent_events: Recent (Event, is_deleted) tuples, most recent first.
|
recent_events: Recent (Event, is_deleted) tuples, most recent first.
|
||||||
sales_stats: Dict with 'total_qty' and 'total_cents' for 30-day sales.
|
sales_stats: Dict with 'total_qty' and 'total_cents' for 30-day sales.
|
||||||
default_quantity: Preserved quantity value on error.
|
default_quantity: Preserved quantity value on error.
|
||||||
default_total_price_cents: Preserved total price value on error.
|
default_total_price_euros: Preserved total price value on error.
|
||||||
default_buyer: Preserved buyer value on error.
|
default_buyer: Preserved buyer value on error.
|
||||||
default_notes: Preserved notes value on error.
|
default_notes: Preserved notes value on error.
|
||||||
|
|
||||||
@@ -363,17 +363,17 @@ def sell_form(
|
|||||||
required=True,
|
required=True,
|
||||||
value=default_quantity or "",
|
value=default_quantity or "",
|
||||||
),
|
),
|
||||||
# Total price in cents
|
# Total price in euros
|
||||||
LabelInput(
|
LabelInput(
|
||||||
"Total Price (cents)",
|
"Total Price (€)",
|
||||||
id="total_price_cents",
|
id="total_price_euros",
|
||||||
name="total_price_cents",
|
name="total_price_euros",
|
||||||
type="number",
|
type="number",
|
||||||
min="0",
|
min="0",
|
||||||
step="1",
|
step="0.01",
|
||||||
placeholder="Total price in cents",
|
placeholder="e.g., 12.50",
|
||||||
required=True,
|
required=True,
|
||||||
value=default_total_price_cents or "",
|
value=default_total_price_euros or "",
|
||||||
),
|
),
|
||||||
# Optional buyer
|
# Optional buyer
|
||||||
LabelInput(
|
LabelInput(
|
||||||
|
|||||||
@@ -365,3 +365,66 @@ class TestEggCollectionAnimalFiltering:
|
|||||||
"Juvenile should NOT be associated with egg collection"
|
"Juvenile should NOT be associated with egg collection"
|
||||||
)
|
)
|
||||||
assert len(associated_ids) == 1, "Only adult females should be associated"
|
assert len(associated_ids) == 1, "Only adult females should be associated"
|
||||||
|
|
||||||
|
|
||||||
|
class TestEggSale:
|
||||||
|
"""Tests for POST /actions/product-sold from eggs page."""
|
||||||
|
|
||||||
|
def test_sell_form_accepts_euros(self, client, seeded_db):
|
||||||
|
"""Price input should accept decimal euros like feed purchase."""
|
||||||
|
resp = client.post(
|
||||||
|
"/actions/product-sold",
|
||||||
|
data={
|
||||||
|
"product_code": "egg.duck",
|
||||||
|
"quantity": "10",
|
||||||
|
"total_price_euros": "12.50", # Euros, not cents
|
||||||
|
"nonce": "test-nonce-sell-euros-1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# Event should store 1250 cents
|
||||||
|
import json
|
||||||
|
|
||||||
|
event_row = seeded_db.execute(
|
||||||
|
"SELECT entity_refs FROM events WHERE type = 'ProductSold' ORDER BY id DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
entity_refs = json.loads(event_row[0])
|
||||||
|
assert entity_refs["total_price_cents"] == 1250
|
||||||
|
|
||||||
|
def test_sell_response_includes_tabs(self, client, seeded_db):
|
||||||
|
"""After recording sale, response should include full page with tabs."""
|
||||||
|
resp = client.post(
|
||||||
|
"/actions/product-sold",
|
||||||
|
data={
|
||||||
|
"product_code": "egg.duck",
|
||||||
|
"quantity": "10",
|
||||||
|
"total_price_euros": "15.00",
|
||||||
|
"nonce": "test-nonce-sell-tabs-1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
# Should have both tabs (proving it's the full eggs page)
|
||||||
|
assert "Harvest" in resp.text
|
||||||
|
assert "Sell" in resp.text
|
||||||
|
|
||||||
|
def test_sell_response_includes_recent_sales(self, client, seeded_db):
|
||||||
|
"""After recording sale, response should include recent sales section."""
|
||||||
|
resp = client.post(
|
||||||
|
"/actions/product-sold",
|
||||||
|
data={
|
||||||
|
"product_code": "egg.duck",
|
||||||
|
"quantity": "10",
|
||||||
|
"total_price_euros": "15.00",
|
||||||
|
"nonce": "test-nonce-sell-recent-1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert "Recent Sales" in resp.text
|
||||||
|
|
||||||
|
def test_sell_form_has_euros_field(self, client):
|
||||||
|
"""Sell form should have total_price_euros field, not total_price_cents."""
|
||||||
|
resp = client.get("/?tab=sell")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert 'name="total_price_euros"' in resp.text
|
||||||
|
assert "Total Price" in resp.text
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ class TestProductSoldFormRendering:
|
|||||||
assert 'name="quantity"' in resp.text or 'id="quantity"' in resp.text
|
assert 'name="quantity"' in resp.text or 'id="quantity"' in resp.text
|
||||||
|
|
||||||
def test_sell_form_has_total_price_field(self, client):
|
def test_sell_form_has_total_price_field(self, client):
|
||||||
"""Form has total_price_cents input field."""
|
"""Form has total_price_euros input field."""
|
||||||
resp = client.get("/sell")
|
resp = client.get("/sell")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert 'name="total_price_cents"' in resp.text or 'id="total_price_cents"' in resp.text
|
assert 'name="total_price_euros"' in resp.text or 'id="total_price_euros"' in resp.text
|
||||||
|
|
||||||
def test_sell_form_has_buyer_field(self, client):
|
def test_sell_form_has_buyer_field(self, client):
|
||||||
"""Form has optional buyer input field."""
|
"""Form has optional buyer input field."""
|
||||||
@@ -89,7 +89,7 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "30",
|
"quantity": "30",
|
||||||
"total_price_cents": "1500",
|
"total_price_euros": "15.00",
|
||||||
"buyer": "Local Market",
|
"buyer": "Local Market",
|
||||||
"notes": "Weekly sale",
|
"notes": "Weekly sale",
|
||||||
"nonce": "test-nonce-sold-1",
|
"nonce": "test-nonce-sold-1",
|
||||||
@@ -113,7 +113,7 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "30",
|
"quantity": "30",
|
||||||
"total_price_cents": "1500",
|
"total_price_euros": "15.00",
|
||||||
"nonce": "test-nonce-sold-2",
|
"nonce": "test-nonce-sold-2",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -136,7 +136,7 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "3",
|
"quantity": "3",
|
||||||
"total_price_cents": "1000",
|
"total_price_euros": "10.00",
|
||||||
"nonce": "test-nonce-sold-3",
|
"nonce": "test-nonce-sold-3",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -158,7 +158,7 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "0",
|
"quantity": "0",
|
||||||
"total_price_cents": "1000",
|
"total_price_euros": "10.00",
|
||||||
"nonce": "test-nonce-sold-4",
|
"nonce": "test-nonce-sold-4",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -172,7 +172,7 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "-1",
|
"quantity": "-1",
|
||||||
"total_price_cents": "1000",
|
"total_price_euros": "10.00",
|
||||||
"nonce": "test-nonce-sold-5",
|
"nonce": "test-nonce-sold-5",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -186,7 +186,7 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "10",
|
"quantity": "10",
|
||||||
"total_price_cents": "-100",
|
"total_price_euros": "-1.00",
|
||||||
"nonce": "test-nonce-sold-6",
|
"nonce": "test-nonce-sold-6",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -199,7 +199,7 @@ class TestProductSold:
|
|||||||
"/actions/product-sold",
|
"/actions/product-sold",
|
||||||
data={
|
data={
|
||||||
"quantity": "10",
|
"quantity": "10",
|
||||||
"total_price_cents": "1000",
|
"total_price_euros": "10.00",
|
||||||
"nonce": "test-nonce-sold-7",
|
"nonce": "test-nonce-sold-7",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -213,30 +213,29 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "invalid.product",
|
"product_code": "invalid.product",
|
||||||
"quantity": "10",
|
"quantity": "10",
|
||||||
"total_price_cents": "1000",
|
"total_price_euros": "10.00",
|
||||||
"nonce": "test-nonce-sold-8",
|
"nonce": "test-nonce-sold-8",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status_code == 422
|
assert resp.status_code == 422
|
||||||
|
|
||||||
def test_product_sold_success_shows_toast(self, client):
|
def test_product_sold_success_returns_full_page(self, client):
|
||||||
"""Successful sale returns response with toast trigger."""
|
"""Successful sale returns full eggs page with tabs."""
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
"/actions/product-sold",
|
"/actions/product-sold",
|
||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "12",
|
"quantity": "12",
|
||||||
"total_price_cents": "600",
|
"total_price_euros": "6.00",
|
||||||
"nonce": "test-nonce-sold-9",
|
"nonce": "test-nonce-sold-9",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
# Check for HX-Trigger header with showToast
|
# Should return full eggs page with tabs (toast via session)
|
||||||
hx_trigger = resp.headers.get("HX-Trigger")
|
assert "Harvest" in resp.text
|
||||||
assert hx_trigger is not None
|
assert "Sell" in resp.text
|
||||||
assert "showToast" in hx_trigger
|
|
||||||
|
|
||||||
def test_product_sold_optional_buyer(self, client, seeded_db):
|
def test_product_sold_optional_buyer(self, client, seeded_db):
|
||||||
"""Buyer field is optional."""
|
"""Buyer field is optional."""
|
||||||
@@ -245,7 +244,7 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "10",
|
"quantity": "10",
|
||||||
"total_price_cents": "500",
|
"total_price_euros": "5.00",
|
||||||
"nonce": "test-nonce-sold-10",
|
"nonce": "test-nonce-sold-10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -265,7 +264,7 @@ class TestProductSold:
|
|||||||
data={
|
data={
|
||||||
"product_code": "egg.duck",
|
"product_code": "egg.duck",
|
||||||
"quantity": "10",
|
"quantity": "10",
|
||||||
"total_price_cents": "500",
|
"total_price_euros": "5.00",
|
||||||
"buyer": "Test Buyer",
|
"buyer": "Test Buyer",
|
||||||
"nonce": "test-nonce-sold-11",
|
"nonce": "test-nonce-sold-11",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user