diff --git a/backend/app/api/admin.py b/backend/app/api/admin.py
index 705b184..5c7543b 100644
--- a/backend/app/api/admin.py
+++ b/backend/app/api/admin.py
@@ -135,10 +135,27 @@ def get_all_meal_plans(
@router.post("/test-email")
def test_email(email: str, db: Session = Depends(get_db)):
+ from app.services.email import get_email_backend
+
+ html = "
MealPlanner Test Email
This is a test email from MealPlanner.
"
+ text = "MealPlanner Test Email\n\nThis is a test email from MealPlanner."
+
+ try:
+ backend = get_email_backend()
+ backend.send(
+ to=email,
+ subject="MealPlanner Test Email",
+ html=html,
+ text=text,
+ )
+ status = "sent"
+ except Exception as e:
+ status = f"failed: {e}"
+
email_log = EmailLog(
recipient_email=email,
template="test",
- status="sent",
+ status=status,
created_at=datetime.now()
)
db.add(email_log)
@@ -146,9 +163,10 @@ def test_email(email: str, db: Session = Depends(get_db)):
db.refresh(email_log)
return {
- "message": "Test email logged",
+ "message": f"Test email {status}",
"email_log_id": str(email_log.id),
- "recipient": email
+ "recipient": email,
+ "status": status,
}
diff --git a/backend/app/api/meals.py b/backend/app/api/meals.py
index b913488..8a5cb60 100644
--- a/backend/app/api/meals.py
+++ b/backend/app/api/meals.py
@@ -156,6 +156,38 @@ def get_vote_page(
safe_meal = _html_escape(meal_type)
action_url = f"/api/meals/vote/{item_id}?token={_html_escape(token, quote=True)}"
+ existing_vote = db.query(MealPlanVote).filter(
+ MealPlanVote.meal_plan_item_id == item_id,
+ MealPlanVote.family_member_id == voter.id,
+ ).first()
+
+ if existing_vote:
+ already_voted_html = f"""
+
+
+
+Already Voted
+
+
+
+
+Hi {safe_voter}, you already voted on this meal
+
+
{safe_recipe}
+
{safe_day} · {safe_meal}
+
+Your vote has already been recorded. Thank you!
+
+
+"""
+ return HTMLResponse(content=already_voted_html, status_code=200)
+
html = f"""
@@ -251,11 +283,11 @@ def submit_vote(
).all()
if any(v.vote is False for v in votes):
- item.approval_status = MealPlanItemStatus.DENIED
+ item.approval_status = MealPlanItemStatus.denied
elif electorate_ids and {v.family_member_id for v in votes} >= electorate_ids:
- item.approval_status = MealPlanItemStatus.APPROVED
+ item.approval_status = MealPlanItemStatus.approved
else:
- item.approval_status = MealPlanItemStatus.PENDING
+ item.approval_status = MealPlanItemStatus.pending
db.commit()
return {"status": "recorded", "item_status": item.approval_status.value}
@@ -282,7 +314,7 @@ def swap_meal_item(item_id: UUID, new_recipe_id: UUID, db: Session = Depends(get
raise HTTPException(status_code=404, detail="New recipe not found")
item.recipe_id = new_recipe_id
- item.approval_status = MealPlanItemStatus.PENDING
+ item.approval_status = MealPlanItemStatus.pending
item.denial_reason = None
item.denial_details = None
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
index 683edf5..0543636 100644
--- a/backend/app/models/__init__.py
+++ b/backend/app/models/__init__.py
@@ -33,11 +33,11 @@ class MealPlanStatus(enum.Enum):
LOCKED = "locked"
-class MealPlanItemStatus(enum.Enum):
- PENDING = "pending"
- APPROVED = "approved"
- DENIED = "denied"
- SWAPPED = "swapped"
+class MealPlanItemStatus(str, enum.Enum):
+ pending = "pending"
+ approved = "approved"
+ denied = "denied"
+ swapped = "swapped"
class ApprovalTokenStatus(enum.Enum):
@@ -221,7 +221,7 @@ class MealPlanItem(Base):
recipe_id = Column(UUID(as_uuid=True), ForeignKey("recipe.id"))
day_of_week = Column(Integer, nullable=False)
meal_type = Column(SQLEnum(MealType, name="meal_type_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj]), nullable=False)
- approval_status = Column(SQLEnum(MealPlanItemStatus, name="meal_plan_item_status_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj]), default=MealPlanItemStatus.PENDING)
+ approval_status = Column(SQLEnum(MealPlanItemStatus, name="meal_plan_item_status_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj]), default=MealPlanItemStatus.pending)
denial_reason = Column(SQLEnum(DenialReason, name="denial_reason_enum", create_type=False, values_callable=lambda obj: [e.value for e in obj]))
denial_details = Column(Text)
estimated_cost = Column(Numeric(10, 2))
diff --git a/backend/app/services/orchestrator/steps.py b/backend/app/services/orchestrator/steps.py
index b64bc37..5488445 100644
--- a/backend/app/services/orchestrator/steps.py
+++ b/backend/app/services/orchestrator/steps.py
@@ -126,7 +126,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
# Preload ingredient names for all pending items
ing_ids: set = set()
for _item in plan.items:
- if _item.approval_status == MealPlanItemStatus.PENDING and _item.recipe:
+ if _item.approval_status == MealPlanItemStatus.pending and _item.recipe:
for _ing in (_item.recipe.ingredients or []):
if "ingredient_id" in _ing:
ing_ids.add(_ing["ingredient_id"])
@@ -141,7 +141,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
# Build shopping list preview once (shared across all member emails)
all_ingredients: dict[str, tuple[str, str, str, str]] = {}
for item in plan.items:
- if item.approval_status != MealPlanItemStatus.PENDING:
+ if item.approval_status != MealPlanItemStatus.pending:
continue
for ing in (item.recipe.ingredients or [] if item.recipe else []):
ing_name = ingredient_names.get(str(ing.get("ingredient_id", "")), ing.get("name", "unknown")).strip()
@@ -193,7 +193,7 @@ def step_email(run: "WeeklyRun", db: "Session") -> None:
for member in members:
item_html_parts = []
for item in plan.items:
- if item.approval_status != MealPlanItemStatus.PENDING:
+ if item.approval_status != MealPlanItemStatus.pending:
continue
token = issue_token(item.id, member.id)
vote_url = (
@@ -317,11 +317,11 @@ def step_deadline(run: "WeeklyRun", db: "Session") -> None:
resolved = 0
for item in plan.items:
- if item.approval_status == MealPlanItemStatus.PENDING:
+ if item.approval_status == MealPlanItemStatus.pending:
item.approval_status = (
- MealPlanItemStatus.APPROVED
+MealPlanItemStatus.approved
if policy == "approve"
- else MealPlanItemStatus.DENIED
+ else MealPlanItemStatus.denied
)
resolved += 1
@@ -351,7 +351,7 @@ def step_finalize(run: "WeeklyRun", db: "Session") -> None:
approved_items = [
item
for item in (plan.items if plan else [])
- if item.approval_status == MealPlanItemStatus.APPROVED
+ if item.approval_status == MealPlanItemStatus.approved
]
members = (
@@ -482,7 +482,7 @@ def step_reminder(run: "WeeklyRun", db: "Session") -> None:
pending_item_ids = [
item.id
for item in plan.items
- if item.approval_status == MealPlanItemStatus.PENDING
+ if item.approval_status == MealPlanItemStatus.pending
]
if not pending_item_ids:
run.reminded_at = datetime.now(timezone.utc)
@@ -512,7 +512,7 @@ def step_reminder(run: "WeeklyRun", db: "Session") -> None:
unvoted = [
item
for item in plan.items
- if item.approval_status == MealPlanItemStatus.PENDING
+ if item.approval_status == MealPlanItemStatus.pending
and item.id not in voted_ids
]
if not unvoted:
diff --git a/backend/app/services/planner/generate.py b/backend/app/services/planner/generate.py
index 78fdb41..28eb00f 100644
--- a/backend/app/services/planner/generate.py
+++ b/backend/app/services/planner/generate.py
@@ -182,7 +182,7 @@ def generate_meal_plan(
recipe_id=scored_recipe.recipe_id,
day_of_week=index + 1, # Mon=1, Tue=2, Wed=3 by default
meal_type=MealType.DINNER,
- approval_status=MealPlanItemStatus.PENDING,
+ approval_status=MealPlanItemStatus.pending,
estimated_cost=scored_recipe.cost.total_cost,
)
db.add(item)
diff --git a/backend/tests/test_orchestrator.py b/backend/tests/test_orchestrator.py
index badc139..01d8dc4 100644
--- a/backend/tests/test_orchestrator.py
+++ b/backend/tests/test_orchestrator.py
@@ -112,7 +112,7 @@ def pending_item(db, meal_plan):
recipe_id=recipe.id,
day_of_week=5,
meal_type=MealType.DINNER,
- approval_status=MealPlanItemStatus.PENDING,
+ approval_status=MealPlanItemStatus.pending,
)
db.add(item)
db.flush()
@@ -467,7 +467,7 @@ def test_step_deadline_resolves_pending_to_approved(db, weekly_run_emailed, meal
db.flush()
step_deadline(weekly_run_emailed, db)
db.refresh(pending_item)
- assert pending_item.approval_status == MealPlanItemStatus.APPROVED
+ assert pending_item.approval_status == MealPlanItemStatus.approved
assert weekly_run_emailed.deadline_passed_at is not None
@@ -478,7 +478,7 @@ def test_step_deadline_resolves_pending_to_denied(db, weekly_run_emailed, meal_p
db.flush()
step_deadline(weekly_run_emailed, db)
db.refresh(pending_item)
- assert pending_item.approval_status == MealPlanItemStatus.DENIED
+ assert pending_item.approval_status == MealPlanItemStatus.denied
# ---------------------------------------------------------------------------
@@ -520,7 +520,7 @@ def approved_item(db, meal_plan):
recipe_id=recipe.id,
day_of_week=6,
meal_type=MealType.DINNER,
- approval_status=MealPlanItemStatus.APPROVED,
+ approval_status=MealPlanItemStatus.approved,
)
db.add(item)
db.flush()