Filter egg harvest events to only include adult female ducks
Males, juveniles, and other non-laying animals were incorrectly being associated with egg collection events. Added life_stage='adult' and sex='female' filters to resolve_ducks_at_location() query. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,9 @@ ar = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[str]:
|
def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[str]:
|
||||||
"""Resolve all duck animal IDs at a location at given timestamp.
|
"""Resolve layer-eligible duck IDs at a location at given timestamp.
|
||||||
|
|
||||||
|
Only includes adult female ducks that can lay eggs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db: Database connection.
|
db: Database connection.
|
||||||
@@ -63,7 +65,7 @@ def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[st
|
|||||||
ts_utc: Timestamp in ms since Unix epoch.
|
ts_utc: Timestamp in ms since Unix epoch.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of animal IDs (ducks at the location, alive at ts_utc).
|
List of animal IDs (adult female ducks at the location, alive at ts_utc).
|
||||||
"""
|
"""
|
||||||
query = """
|
query = """
|
||||||
SELECT DISTINCT ali.animal_id
|
SELECT DISTINCT ali.animal_id
|
||||||
@@ -74,6 +76,8 @@ def resolve_ducks_at_location(db: Any, location_id: str, ts_utc: int) -> list[st
|
|||||||
AND (ali.end_utc IS NULL OR ali.end_utc > ?)
|
AND (ali.end_utc IS NULL OR ali.end_utc > ?)
|
||||||
AND ar.species_code = 'duck'
|
AND ar.species_code = 'duck'
|
||||||
AND ar.status = 'alive'
|
AND ar.status = 'alive'
|
||||||
|
AND ar.life_stage = 'adult'
|
||||||
|
AND ar.sex = 'female'
|
||||||
ORDER BY ali.animal_id
|
ORDER BY ali.animal_id
|
||||||
"""
|
"""
|
||||||
rows = db.execute(query, (location_id, ts_utc, ts_utc)).fetchall()
|
rows = db.execute(query, (location_id, ts_utc, ts_utc)).fetchall()
|
||||||
|
|||||||
@@ -278,3 +278,90 @@ class TestEggsRecentEvents:
|
|||||||
|
|
||||||
# The response should contain a link to the event detail
|
# The response should contain a link to the event detail
|
||||||
assert f"/events/{event_id}" in resp.text
|
assert f"/events/{event_id}" in resp.text
|
||||||
|
|
||||||
|
|
||||||
|
class TestEggCollectionAnimalFiltering:
|
||||||
|
"""Tests that egg collection only associates adult females."""
|
||||||
|
|
||||||
|
def test_egg_collection_excludes_males_and_juveniles(
|
||||||
|
self, client, seeded_db, location_strip1_id
|
||||||
|
):
|
||||||
|
"""Egg collection only associates adult female ducks, not males or juveniles."""
|
||||||
|
# Setup: Create mixed animals at location
|
||||||
|
event_store = EventStore(seeded_db)
|
||||||
|
registry = ProjectionRegistry()
|
||||||
|
registry.register(AnimalRegistryProjection(seeded_db))
|
||||||
|
registry.register(EventAnimalsProjection(seeded_db))
|
||||||
|
registry.register(IntervalProjection(seeded_db))
|
||||||
|
registry.register(ProductsProjection(seeded_db))
|
||||||
|
|
||||||
|
animal_service = AnimalService(seeded_db, event_store, registry)
|
||||||
|
ts_utc = int(time.time() * 1000)
|
||||||
|
|
||||||
|
# Create adult female (should be included)
|
||||||
|
female_payload = AnimalCohortCreatedPayload(
|
||||||
|
species="duck",
|
||||||
|
count=1,
|
||||||
|
life_stage="adult",
|
||||||
|
sex="female",
|
||||||
|
location_id=location_strip1_id,
|
||||||
|
origin="purchased",
|
||||||
|
)
|
||||||
|
female_event = animal_service.create_cohort(female_payload, ts_utc, "test_user")
|
||||||
|
female_id = female_event.entity_refs["animal_ids"][0]
|
||||||
|
|
||||||
|
# Create adult male (should be excluded)
|
||||||
|
male_payload = AnimalCohortCreatedPayload(
|
||||||
|
species="duck",
|
||||||
|
count=1,
|
||||||
|
life_stage="adult",
|
||||||
|
sex="male",
|
||||||
|
location_id=location_strip1_id,
|
||||||
|
origin="purchased",
|
||||||
|
)
|
||||||
|
male_event = animal_service.create_cohort(male_payload, ts_utc, "test_user")
|
||||||
|
male_id = male_event.entity_refs["animal_ids"][0]
|
||||||
|
|
||||||
|
# Create juvenile female (should be excluded)
|
||||||
|
juvenile_payload = AnimalCohortCreatedPayload(
|
||||||
|
species="duck",
|
||||||
|
count=1,
|
||||||
|
life_stage="juvenile",
|
||||||
|
sex="female",
|
||||||
|
location_id=location_strip1_id,
|
||||||
|
origin="purchased",
|
||||||
|
)
|
||||||
|
juvenile_event = animal_service.create_cohort(juvenile_payload, ts_utc, "test_user")
|
||||||
|
juvenile_id = juvenile_event.entity_refs["animal_ids"][0]
|
||||||
|
|
||||||
|
# Collect eggs
|
||||||
|
resp = client.post(
|
||||||
|
"/actions/product-collected",
|
||||||
|
data={
|
||||||
|
"location_id": location_strip1_id,
|
||||||
|
"quantity": "6",
|
||||||
|
"nonce": "test-nonce-filter",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
# Get the egg collection event
|
||||||
|
event_row = seeded_db.execute(
|
||||||
|
"SELECT id FROM events WHERE type = 'ProductCollected' ORDER BY id DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
event_id = event_row[0]
|
||||||
|
|
||||||
|
# Check which animals are associated with the event
|
||||||
|
animal_rows = seeded_db.execute(
|
||||||
|
"SELECT animal_id FROM event_animals WHERE event_id = ?",
|
||||||
|
(event_id,),
|
||||||
|
).fetchall()
|
||||||
|
associated_ids = {row[0] for row in animal_rows}
|
||||||
|
|
||||||
|
# Only the adult female should be associated
|
||||||
|
assert female_id in associated_ids, "Adult female should be associated with egg collection"
|
||||||
|
assert male_id not in associated_ids, "Male should NOT be associated with egg collection"
|
||||||
|
assert juvenile_id not in associated_ids, (
|
||||||
|
"Juvenile should NOT be associated with egg collection"
|
||||||
|
)
|
||||||
|
assert len(associated_ids) == 1, "Only adult females should be associated"
|
||||||
|
|||||||
Reference in New Issue
Block a user