6.1 KiB
Feature: Chore and Reward states expire at the end of the day.
Overview
Chores and rewards can be pending, too late, etc. At the start of a new day, the state of chores and rewards should be reset. Notifications in the notification nav selector should be cleared out as well.
Goal: Chores and reward states get reset at the start of a new day.
User Story: As a parent, if I forget to approve/reject a pending chore or reward by the next day, then those items will have to be selected again by the child to go back to pending. As a parent, if a scheduled chore is too late, the next day that chore will reset, but still follow it's schedule.
Rules: Create frontend and backend unit tests
Data Model Changes
Backend Model
No model changes were required. PendingConfirmation already inherits created_at: float (Unix timestamp) from BaseModel, which is sufficient to determine whether a record is from a prior calendar day.
Frontend Model
No model changes required. The ChildChoreConfirmationPayload and ChildRewardRequestEventPayload types are unchanged; the existing RESET and CANCELLED operations are reused.
Backend Implementation
A new hourly scheduler (backend/utils/state_expiry_scheduler.py) runs idempotently to expire stale pending records:
_get_today_start_timestamp(tz_str)computes the Unix timestamp for midnight today in the user's IANA timezone, falling back to UTC.expire_stale_pending_for_user(user_id, tz_str)queriespending_confirmations_dbfor records wherestatus='pending'andcreated_at < today_startfor that user. Each matching record is deleted from the database, then an SSE event is fired:CHILD_CHORE_CONFIRMATION(RESET)for chores,CHILD_REWARD_REQUEST(CANCELLED)for rewards. Usessend_event_to_userdirectly since there is no HTTP request context.run_state_expiry_check(app)iterates all verified users and callsexpire_stale_pending_for_userfor each. Skips entirely whenDB_ENV=e2e.start_state_expiry_scheduler(app)registers the hourly cron job via APScheduler and also runs an immediate check on startup so any records that expired while the server was down are cleared promptly.
The scheduler is registered in backend/main.py alongside the existing digest and deletion schedulers.
Backend Tests
Tests live in backend/tests/test_state_expiry_scheduler.py. Coverage:
_get_today_start_timestampreturns a float before now, falls back to UTC onNoneor invalid timezone, and shifts midnight correctly across timezones.- Pending chore from yesterday is deleted and fires a
CHILD_CHORE_CONFIRMATION(RESET)SSE event with the correcttask_id. - Pending reward from yesterday is deleted and fires a
CHILD_REWARD_REQUEST(CANCELLED)SSE event with the correctreward_id. - Pending record created today is not deleted.
- Record with
status='approved'from yesterday is not deleted. - User with no pending records produces no errors and fires no SSE events.
- Multiple stale records for one user are all expired in a single call.
run_state_expiry_checkexpires records for all verified users across multiple users.- Only stale records are expired; fresh records from today are preserved.
DB_ENV=e2ecauses the scheduler to skip all processing.
Frontend Implementation
Two files were updated to handle the RESET operation fired when chore pending states expire:
NotificationView.vue: Added'RESET'to thechild_chore_confirmationoperation allow-list so the notification list refreshes when a pending chore confirmation is expired by the scheduler.ParentLayout.vue: Added'RESET'to thechild_chore_confirmationhandler allow-list so the notification badge count is re-fetched when a pending chore confirmation is expired.
The child view (ChildView.vue) required no changes — it already refreshes on any child_chore_confirmation operation regardless of the operation value.
Frontend Tests
Tests live in src/components/__tests__/NotificationView.spec.ts and src/components/__tests__/ParentLayout.spec.ts.
NotificationView.vueincrementsrefreshKeyon each ofCONFIRMED,APPROVED,REJECTED,CANCELLED, andRESEToperations forchild_chore_confirmation.NotificationView.vuedoes not incrementrefreshKeyon unknown operations.NotificationView.vueincrementsrefreshKeyonCREATED,CANCELLED, andGRANTEDoperations forchild_reward_request.ParentLayout.vuere-fetches the notification count on each ofCONFIRMED,APPROVED,REJECTED,CANCELLED, andRESEToperations forchild_chore_confirmation.ParentLayout.vuedoes not re-fetch on unknown operations.ParentLayout.vuere-fetches the notification count onCREATED,CANCELLED, andGRANTEDoperations forchild_reward_request.
Future Considerations
- Push notifications (Web Push) currently reference pending items; expired records will not trigger a push notification removal. A future improvement could send a silent push to dismiss stale items from the notification tray.
- If digest emails are scheduled near midnight, the expiry scheduler may clear items before the digest runs. The digest scheduler already skips empty pending lists gracefully, so no action is needed, but the ordering could be made explicit.
Acceptance Criteria (Definition of Done)
Backend
- Hourly scheduler deletes
PendingConfirmationrecords withstatus='pending'created on a prior calendar day (per user timezone) CHILD_CHORE_CONFIRMATION(RESET)SSE event is fired for each expired chore recordCHILD_REWARD_REQUEST(CANCELLED)SSE event is fired for each expired reward record- Scheduler skips processing when
DB_ENV=e2e - Scheduler runs on startup to handle records that expired during downtime
status='approved'records are not deleted- Records created today are not deleted
- Backend unit tests pass (17 tests)
Frontend
- Notification badge in
ParentLayout.vuere-fetches count onRESEToperation - Notification list in
NotificationView.vuerefreshes onRESEToperation - Frontend unit tests pass (18 tests)