Allow recording zero eggs collected
All checks were successful
Deploy / deploy (push) Successful in 1m37s
All checks were successful
Deploy / deploy (push) Successful in 1m37s
Enable recording "checked coop, found 0 eggs" to distinguish from days when the coop wasn't checked at all. Statistics remain eggs/calendar day. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -308,7 +308,7 @@ class ProductCollectedPayload(BaseModel):
|
|||||||
|
|
||||||
location_id: str = Field(..., min_length=26, max_length=26)
|
location_id: str = Field(..., min_length=26, max_length=26)
|
||||||
product_code: str
|
product_code: str
|
||||||
quantity: int = Field(..., ge=1)
|
quantity: int = Field(..., ge=0) # 0 allowed: checked but found none
|
||||||
resolved_ids: list[str] = Field(..., min_length=1)
|
resolved_ids: list[str] = Field(..., min_length=1)
|
||||||
notes: str | None = None
|
notes: str | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -376,9 +376,9 @@ async def product_collected(request: Request, session):
|
|||||||
request, db, locations, products, location_id, "Quantity must be a number"
|
request, db, locations, products, location_id, "Quantity must be a number"
|
||||||
)
|
)
|
||||||
|
|
||||||
if quantity < 1:
|
if quantity < 0:
|
||||||
return _render_harvest_error(
|
return _render_harvest_error(
|
||||||
request, db, locations, products, location_id, "Quantity must be at least 1"
|
request, db, locations, products, location_id, "Quantity cannot be negative"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get timestamp - use provided or current (supports backdating)
|
# Get timestamp - use provided or current (supports backdating)
|
||||||
|
|||||||
@@ -184,13 +184,13 @@ def harvest_form(
|
|||||||
id="location_id",
|
id="location_id",
|
||||||
name="location_id",
|
name="location_id",
|
||||||
),
|
),
|
||||||
# Quantity input (integer only, min=1)
|
# Quantity input (integer only, 0 allowed for "checked but found none")
|
||||||
LabelInput(
|
LabelInput(
|
||||||
"Quantity",
|
"Quantity",
|
||||||
id="quantity",
|
id="quantity",
|
||||||
name="quantity",
|
name="quantity",
|
||||||
type="number",
|
type="number",
|
||||||
min="1",
|
min="0",
|
||||||
step="1",
|
step="1",
|
||||||
placeholder="Number of eggs",
|
placeholder="Number of eggs",
|
||||||
required=True,
|
required=True,
|
||||||
|
|||||||
@@ -285,15 +285,27 @@ class TestProductPayloads:
|
|||||||
)
|
)
|
||||||
assert payload.quantity == 12
|
assert payload.quantity == 12
|
||||||
|
|
||||||
def test_quantity_must_be_positive(self):
|
def test_quantity_zero_is_valid(self):
|
||||||
"""quantity must be >= 1."""
|
"""quantity=0 is valid (checked but found none)."""
|
||||||
|
from animaltrack.events.payloads import ProductCollectedPayload
|
||||||
|
|
||||||
|
payload = ProductCollectedPayload(
|
||||||
|
location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
||||||
|
product_code="egg.duck",
|
||||||
|
quantity=0,
|
||||||
|
resolved_ids=["01ARZ3NDEKTSV4RRFFQ69G5FAV"],
|
||||||
|
)
|
||||||
|
assert payload.quantity == 0
|
||||||
|
|
||||||
|
def test_quantity_cannot_be_negative(self):
|
||||||
|
"""quantity must be >= 0."""
|
||||||
from animaltrack.events.payloads import ProductCollectedPayload
|
from animaltrack.events.payloads import ProductCollectedPayload
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
ProductCollectedPayload(
|
ProductCollectedPayload(
|
||||||
location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
location_id="01ARZ3NDEKTSV4RRFFQ69G5FAV",
|
||||||
product_code="egg.duck",
|
product_code="egg.duck",
|
||||||
quantity=0,
|
quantity=-1,
|
||||||
resolved_ids=["01ARZ3NDEKTSV4RRFFQ69G5FAV"],
|
resolved_ids=["01ARZ3NDEKTSV4RRFFQ69G5FAV"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -137,10 +137,10 @@ class TestEggCollection:
|
|||||||
assert event_row is not None
|
assert event_row is not None
|
||||||
assert event_row[0] == "ProductCollected"
|
assert event_row[0] == "ProductCollected"
|
||||||
|
|
||||||
def test_egg_collection_validation_quantity_zero(
|
def test_egg_collection_quantity_zero_accepted(
|
||||||
self, client, location_strip1_id, ducks_at_strip1
|
self, client, seeded_db, location_strip1_id, ducks_at_strip1
|
||||||
):
|
):
|
||||||
"""quantity=0 returns 422."""
|
"""quantity=0 is accepted (checked coop, found no eggs)."""
|
||||||
resp = client.post(
|
resp = client.post(
|
||||||
"/actions/product-collected",
|
"/actions/product-collected",
|
||||||
data={
|
data={
|
||||||
@@ -150,7 +150,17 @@ class TestEggCollection:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert resp.status_code == 422
|
assert resp.status_code in [200, 302, 303]
|
||||||
|
|
||||||
|
# Verify event was created with quantity=0
|
||||||
|
event_row = seeded_db.execute(
|
||||||
|
"SELECT payload FROM events WHERE type = 'ProductCollected' ORDER BY id DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
assert event_row is not None
|
||||||
|
import json
|
||||||
|
|
||||||
|
payload = json.loads(event_row[0])
|
||||||
|
assert payload["quantity"] == 0
|
||||||
|
|
||||||
def test_egg_collection_validation_quantity_negative(
|
def test_egg_collection_validation_quantity_negative(
|
||||||
self, client, location_strip1_id, ducks_at_strip1
|
self, client, location_strip1_id, ducks_at_strip1
|
||||||
|
|||||||
Reference in New Issue
Block a user