diff --git a/backend/api/child_api.py b/backend/api/child_api.py index dd89141..715e578 100644 --- a/backend/api/child_api.py +++ b/backend/api/child_api.py @@ -1,5 +1,5 @@ from time import sleep -from datetime import date as date_type, datetime, timezone +from datetime import datetime, timezone from flask import Blueprint, request, jsonify from tinydb import Query @@ -31,7 +31,7 @@ from models.tracking_event import TrackingEvent from utils.tracking_logger import log_tracking_event from collections import defaultdict from db.chore_schedules import get_schedule -from db.task_extensions import get_extension +from db.task_extensions import get_extension_for_child_task import logging child_api = Blueprint('child_api', __name__) @@ -288,8 +288,7 @@ def list_child_tasks(id): if task_obj.type == 'chore': schedule = get_schedule(id, tid) ct_dict['schedule'] = schedule.to_dict() if schedule else None - today_str = date_type.today().isoformat() - ext = get_extension(id, tid, today_str) + ext = get_extension_for_child_task(id, tid) ct_dict['extension_date'] = ext.date if ext else None # Attach pending confirmation status for chores diff --git a/backend/api/chore_schedule_api.py b/backend/api/chore_schedule_api.py index 26949ff..398b8a9 100644 --- a/backend/api/chore_schedule_api.py +++ b/backend/api/chore_schedule_api.py @@ -160,11 +160,14 @@ def extend_chore_time(child_id, task_id): if not date or not isinstance(date, str): return jsonify({'error': 'date is required (ISO date string)', 'code': ErrorCodes.MISSING_FIELD}), 400 - # 409 if already extended for this date + # 409 if already extended for this exact date existing = get_extension(child_id, task_id, date) if existing: return jsonify({'error': 'Chore already extended for this date', 'code': 'ALREADY_EXTENDED'}), 409 + # Clear any prior extension for this child+task before inserting the new one + # so stale records from previous dates do not accumulate. + delete_extension_for_child_task(child_id, task_id) extension = TaskExtension(child_id=child_id, task_id=task_id, date=date) add_extension(extension) diff --git a/backend/db/task_extensions.py b/backend/db/task_extensions.py index 745b3a3..d2ef770 100644 --- a/backend/db/task_extensions.py +++ b/backend/db/task_extensions.py @@ -30,3 +30,20 @@ def delete_extensions_for_task(task_id: str) -> None: def delete_extension_for_child_task(child_id: str, task_id: str) -> None: q = Query() task_extensions_db.remove((q.child_id == child_id) & (q.task_id == task_id)) + + +def get_extension_for_child_task(child_id: str, task_id: str) -> TaskExtension | None: + """Return the extension for a child+task without filtering by date. + + Avoids timezone mismatches between the server (UTC) and the client's local + date. The caller (or the frontend) is responsible for deciding whether the + returned extension_date is still applicable. + """ + q = Query() + results = task_extensions_db.search( + (q.child_id == child_id) & (q.task_id == task_id) + ) + if not results: + return None + latest = max(results, key=lambda r: r.get('date', '')) + return TaskExtension.from_dict(latest) diff --git a/backend/tests/test_child_api.py b/backend/tests/test_child_api.py index ce2286f..a73a769 100644 --- a/backend/tests/test_child_api.py +++ b/backend/tests/test_child_api.py @@ -439,6 +439,29 @@ def test_list_child_tasks_returns_extension_date_when_set(client): assert tasks[TASK_GOOD_ID]['extension_date'] == today +def test_list_child_tasks_returns_extension_date_for_yesterday(client): + """Regression: extension inserted for yesterday's date must still be returned. + + Previously, list-tasks used date_type.today() on the server to look up + extensions, causing a timezone mismatch when the client's local date + differed from the server's UTC date. The fix removes the date filter so + any stored extension is returned regardless of which date it was created + for. + """ + _setup_sched_child_and_tasks(task_db, child_db) + from datetime import date as date_type_local, timedelta + yesterday = (date_type_local.today() - timedelta(days=1)).isoformat() + task_extensions_db.insert({ + 'id': 'ext-yesterday', + 'child_id': CHILD_SCHED_ID, + 'task_id': TASK_GOOD_ID, + 'date': yesterday, + }) + resp = client.get(f'/child/{CHILD_SCHED_ID}/list-tasks') + tasks = {t['id']: t for t in resp.get_json()['tasks']} + assert tasks[TASK_GOOD_ID]['extension_date'] == yesterday + + def test_list_child_tasks_extension_date_null_when_not_set(client): """Good chore with no extension returns extension_date=null.""" _setup_sched_child_and_tasks(task_db, child_db)