Files
chore/.github/specs/archive/feat-state-expire.md
Ryan Kegel 8907184fde
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m59s
Add chore expiry notification system with scheduling and tests
- Implemented `trigger_chore_expiry.py` script to manually trigger chore expiry notifications for users via the admin API.
- Developed `chore_expiry_notification_scheduler.py` to handle the logic for sending notifications for chores expiring within the next 75 minutes.
- Created utility functions in `schedule_utils.py` to determine scheduling and deadlines for chores.
- Added comprehensive tests for the chore expiry notification system in `test_chore_expiry_notification_scheduler.py`, covering various scenarios including scheduled chores, confirmations, and user settings.
2026-04-22 15:37:40 -04:00

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) queries pending_confirmations_db for records where status='pending' and created_at < today_start for 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. Uses send_event_to_user directly since there is no HTTP request context.
  • run_state_expiry_check(app) iterates all verified users and calls expire_stale_pending_for_user for each. Skips entirely when DB_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_timestamp returns a float before now, falls back to UTC on None or 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 correct task_id.
  • Pending reward from yesterday is deleted and fires a CHILD_REWARD_REQUEST(CANCELLED) SSE event with the correct reward_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_check expires records for all verified users across multiple users.
  • Only stale records are expired; fresh records from today are preserved.
  • DB_ENV=e2e causes 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 the child_chore_confirmation operation allow-list so the notification list refreshes when a pending chore confirmation is expired by the scheduler.
  • ParentLayout.vue: Added 'RESET' to the child_chore_confirmation handler 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.vue increments refreshKey on each of CONFIRMED, APPROVED, REJECTED, CANCELLED, and RESET operations for child_chore_confirmation.
  • NotificationView.vue does not increment refreshKey on unknown operations.
  • NotificationView.vue increments refreshKey on CREATED, CANCELLED, and GRANTED operations for child_reward_request.
  • ParentLayout.vue re-fetches the notification count on each of CONFIRMED, APPROVED, REJECTED, CANCELLED, and RESET operations for child_chore_confirmation.
  • ParentLayout.vue does not re-fetch on unknown operations.
  • ParentLayout.vue re-fetches the notification count on CREATED, CANCELLED, and GRANTED operations for child_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 PendingConfirmation records with status='pending' created on a prior calendar day (per user timezone)
  • CHILD_CHORE_CONFIRMATION(RESET) SSE event is fired for each expired chore record
  • CHILD_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.vue re-fetches count on RESET operation
  • Notification list in NotificationView.vue refreshes on RESET operation
  • Frontend unit tests pass (18 tests)