All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m59s
- 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.
149 lines
9.7 KiB
Markdown
149 lines
9.7 KiB
Markdown
# Feature: Push notification for chores that are going to expire in the next hour.
|
|
|
|
## Overview
|
|
|
|
When a scheduled chore is going to expire within the next hour and it has not be marked completed or pending, a push notification should go out to tell the user that the scheduled chore needs to be finished soon.
|
|
|
|
**Goal:** When a chore is about to expire (within the hour) send a push notification to the user and allow the user to acknowlege it.
|
|
|
|
**User Story:**
|
|
As a parent, I should receive a notification when a scheduled chore is going to expire soon. When I click on the notification, I should be brought to the page with the chore.
|
|
|
|
**Rules:**
|
|
|
|
1. If there are multiple chores that are going to expire, present them in the push notification as well.
|
|
2. The notification should show which child the chore belongs to.
|
|
3. If a chore is scheduled to expire between 9:00pm to 9:15pm, the notification for it should go out at 8:00pm. That should give the earlier hour expiration plenty of notice.
|
|
|
|
**Questions:**
|
|
|
|
1. If there are multiple chores in the push, should we say there are multiple chores ending next hour or should we list each by name?
|
|
2. Should we section the notification if there are multiple children? [ex. Child1 chores: ... Child2 chores: ...]
|
|
3. Should we reuse one of the other hourly schedulers or create a new one? should we refactor and have one hourly scheduler that does multiple operations?
|
|
|
|
---
|
|
|
|
## Data Model Changes
|
|
|
|
### Backend Model
|
|
|
|
No new models required. The feature reuses existing models:
|
|
|
|
- `ChoreSchedule` — provides schedule mode, day configs, deadline times, and enabled state
|
|
- `PendingConfirmation` — queried to determine if a chore is already pending or approved (either status means the child has acted on it; no notification is needed)
|
|
|
|
### Frontend Model
|
|
|
|
No changes to TypeScript interfaces.
|
|
|
|
---
|
|
|
|
## Backend Implementation
|
|
|
|
**`backend/utils/schedule_utils.py`** (new)
|
|
|
|
A Python port of the three core functions from `frontend/vue-app/src/common/scheduleUtils.ts`. The frontend is the authoritative source for schedule math; this file mirrors that logic server-side so the scheduler can determine which chores are active and when they expire without making HTTP calls or duplicating logic ad-hoc.
|
|
|
|
- `interval_hits_today(anchor_date, interval_days, local_date)` — returns True if an interval-mode schedule fires on the given local date
|
|
- `is_scheduled_today(schedule_dict, local_date)` — returns True for days-mode (weekday match) or interval-mode (anchor + interval math); paused schedules always return True
|
|
- `get_due_time_today(schedule_dict, local_date)` — returns `(hour, minute)` or `None` (None means Anytime or paused — no expiry)
|
|
|
|
Note on weekday convention: the app uses JS/Python convention where Sunday = 0 and Saturday = 6 (same as `DayConfig.day`). Python's `date.weekday()` returns Monday = 0, so a conversion of `(weekday + 1) % 7` is applied when comparing.
|
|
|
|
**`backend/utils/chore_expiry_notification_scheduler.py`** (new)
|
|
|
|
- `get_expiring_chores_for_user(user_id, tz_str, now_dt)` — loads all children for the user, iterates their chore tasks, checks for an active schedule with a deadline falling in `(now, now + 75 min]`, and excludes any chore that already has a `PendingConfirmation` record (any status). Returns a list of dicts with child and task metadata.
|
|
- `_build_push_payload(expiring)` — constructs the push payload. Single chore: title names the chore and deep-link fields are set for direct navigation. Multiple chores: generic title, `child_id` and `entity_id` are null, and the body lists chores grouped by child name (e.g. `Alex: Clean Room, Make Bed\nSam: Trash`).
|
|
- `send_chore_expiry_notifications_for_user(user_id, tz_str)` — calls `get_expiring_chores_for_user`, builds the payload, and calls `send_push_to_user`.
|
|
- `run_chore_expiry_check(app)` — skips the e2e environment, loops all verified users, skips users with `push_notifications_enabled=False`, and calls `send_chore_expiry_notifications_for_user` for each eligible user.
|
|
- `start_chore_expiry_notification_scheduler(app)` — registers an APScheduler `cron` job at `minute=0` (top of every hour), consistent with the existing schedulers.
|
|
|
|
**Push payload type:** `chore_expiring_soon`. The absence of `approve_token` and `deny_token` fields is intentional and is what triggers the service worker to suppress action buttons.
|
|
|
|
**`backend/main.py`** (modified)
|
|
|
|
Import added and `start_chore_expiry_notification_scheduler(app)` called after the existing schedulers.
|
|
|
|
## Backend Tests
|
|
|
|
- [x] `get_expiring_chores_for_user`: chore in 75-min window → included
|
|
- [x] `get_expiring_chores_for_user`: deadline in the past → excluded
|
|
- [x] `get_expiring_chores_for_user`: deadline beyond 75-min window → excluded
|
|
- [x] `get_expiring_chores_for_user`: Anytime schedule (no deadline) → excluded
|
|
- [x] `get_expiring_chores_for_user`: chore not scheduled today (wrong weekday) → excluded
|
|
- [x] `get_expiring_chores_for_user`: status=pending confirmation exists → excluded
|
|
- [x] `get_expiring_chores_for_user`: status=approved confirmation exists → excluded
|
|
- [x] `get_expiring_chores_for_user`: no schedule → excluded
|
|
- [x] `get_expiring_chores_for_user`: paused schedule (enabled=False) → excluded
|
|
- [x] `get_expiring_chores_for_user`: interval mode hits today → included
|
|
- [x] `get_expiring_chores_for_user`: interval mode misses today → excluded
|
|
- [x] `get_expiring_chores_for_user`: multiple children returns entries for both
|
|
- [x] `_build_push_payload`: single chore → named title, deep-link fields set
|
|
- [x] `_build_push_payload`: multiple chores → generic title, child_id/entity_id null
|
|
- [x] `_build_push_payload`: body groups chore names by child
|
|
- [x] `_build_push_payload`: no approve_token or deny_token in payload
|
|
- [x] `send_chore_expiry_notifications_for_user`: calls send_push_to_user with correct payload
|
|
- [x] `send_chore_expiry_notifications_for_user`: no push when no chores expiring
|
|
- [x] `run_chore_expiry_check`: skips when DB_ENV=e2e
|
|
- [x] `run_chore_expiry_check`: skips user with push_notifications_enabled=False
|
|
- [x] `run_chore_expiry_check`: skips unverified user
|
|
- [x] `run_chore_expiry_check`: sends push for eligible verified user with push enabled
|
|
- [x] `schedule_utils`: interval_hits_today — same day as anchor hits
|
|
- [x] `schedule_utils`: interval_hits_today — correct interval day hits
|
|
- [x] `schedule_utils`: interval_hits_today — non-interval day misses
|
|
- [x] `schedule_utils`: interval_hits_today — date before anchor misses
|
|
- [x] `schedule_utils`: interval_hits_today — empty anchor hits today
|
|
- [x] `schedule_utils`: is_scheduled_today — days mode matching weekday
|
|
- [x] `schedule_utils`: is_scheduled_today — days mode non-matching weekday
|
|
- [x] `schedule_utils`: is_scheduled_today — paused always true
|
|
- [x] `schedule_utils`: is_scheduled_today — interval mode hit
|
|
- [x] `schedule_utils`: is_scheduled_today — interval mode miss
|
|
- [x] `schedule_utils`: get_due_time_today — days mode returns (hour, minute)
|
|
- [x] `schedule_utils`: get_due_time_today — Anytime returns None
|
|
- [x] `schedule_utils`: get_due_time_today — wrong day returns None
|
|
- [x] `schedule_utils`: get_due_time_today — paused returns None
|
|
- [x] `schedule_utils`: get_due_time_today — interval mode returns (hour, minute)
|
|
- [x] `schedule_utils`: get_due_time_today — interval Anytime returns None
|
|
|
|
---
|
|
|
|
## Frontend Implementation
|
|
|
|
**`frontend/vue-app/public/sw.js`** (modified)
|
|
|
|
In the `push` event handler, the `actions` array passed to `showNotification` is now conditionally set: when `payload.type === 'chore_expiring_soon'` the array is empty (no buttons); otherwise Approve and Deny buttons are shown as before. The `notificationclick` handler requires no changes — tapping the notification body already navigates to `/parent/{child_id}?scrollTo={entity_id}` when those fields are present, and falls back to `/parent` when they are null (multi-chore case).
|
|
|
|
## Frontend Tests
|
|
|
|
No automated tests added. The `sw.js` change is a simple conditional and is covered by manual verification: triggering a `chore_expiring_soon` push and confirming no action buttons appear in the browser notification, and that tapping opens the correct URL.
|
|
|
|
---
|
|
|
|
## Future Considerations
|
|
|
|
- **75-minute window double-notification**: A chore deadline that falls in the overlap of two consecutive 75-minute windows (e.g. 9:10 pm appears in both the 8:00 pm run's window and the 9:00 pm run's window) will produce two notifications. This is a known and accepted trade-off; no deduplication is applied. A future improvement could track notified chores per day in a lightweight DB table to prevent repeats.
|
|
- **Task extensions**: `TaskExtension` sets an `extension_date` that allows a chore to carry over to another day. Extended chores are treated identically to regular scheduled chores — the schedule deadline time is used as-is. A future enhancement could optionally adjust the deadline time for extended chores.
|
|
|
|
---
|
|
|
|
## Acceptance Criteria (Definition of Done)
|
|
|
|
### Backend
|
|
|
|
- [x] `backend/utils/schedule_utils.py` created — Python port of schedule math from `scheduleUtils.ts`
|
|
- [x] `backend/utils/chore_expiry_notification_scheduler.py` created — hourly scheduler with 75-min look-ahead window
|
|
- [x] Chores with `Anytime` schedule (no deadline) are not notified
|
|
- [x] Chores with a `pending` or `approved` confirmation are not notified
|
|
- [x] Chores on paused schedules are not notified
|
|
- [x] Users with `push_notifications_enabled=False` are skipped
|
|
- [x] Unverified users are skipped
|
|
- [x] e2e environment is skipped
|
|
- [x] Scheduler registered in `backend/main.py`
|
|
- [x] All backend tests pass
|
|
|
|
### Frontend
|
|
|
|
- [x] `sw.js` updated — `chore_expiring_soon` notifications show no Approve/Deny action buttons
|
|
- [x] Tapping a single-chore notification navigates to the child's chore page
|
|
- [x] Tapping a multi-chore notification navigates to `/parent`
|