Notifications & Edge Cases
Part of: Pickatale Master Build Wiki | Version: v1.7 | Last Updated: 2026-04-18
22. Notification Triggers
Source: (D) | Status: Confirmed as spec Delivery: SendGrid (email) for adults. In-app (teacher portal, parent portal) for notifications. No email to children.
| ID | Trigger | Channel | Recipient | Template Variables |
|---|---|---|---|---|
| VERIFY_EMAIL | User registers | Registrant | name, verify_url, expires_in | |
| WELCOME_TEACHER | Email verified (teacher) | Teacher | name, portal_url | |
| WELCOME_PARENT | Email verified (parent) | Parent | name, portal_url | |
| INVITE_EMAIL | Teacher invites colleague | Invitee | inviter_name, school_name, invite_url, expires_in | |
| PASSWORD_RESET | User requests reset | User | name, reset_url, expires_in | |
| PASSWORD_CHANGED | Password reset completed | User | name, timestamp | |
| ACCOUNT_LOCKED_ALERT | 5 failed login attempts | User | name, unlock_time | |
| SCHOOL_SUBSCRIPTION_EXPIRED | School subscription ends | School admin + all teachers | school_name, expired_date, renewal_url, data_export_url | |
| PARENT_CLAIM_REQUEST | Parent claims teacher's student | In-app (Teacher Portal) | Teacher | parent_name, child_name, approve_url |
| PARENT_CLAIM_APPROVED | Teacher approves parent claim | Parent | child_name, portal_url | |
| TEACHER_ALERT_LOW_SCORE | Bot detects avg quiz <65% on 2+ quizzes | In-app (Teacher Portal) | Class teacher | child_name, avg_score, report_url |
| TEACHER_ALERT_INACTIVE | Child inactive 4+ days | In-app (Teacher Portal) | Class teacher | child_name, last_active, report_url |
| TEACHER_ALERT_NO_PLACEMENT | Child not completed placement test | In-app (Teacher Portal) | Class teacher | child_name |
| WEEKLY_PARENT_DIGEST | Every Sunday 08:00 UTC | Parent | child_name, week_summary, highlights, home_tip, portal_url | |
| NIGHTLY_PARENT_DIGEST | After Learner Bot nightly cycle (04:00 UTC) | Email (optional β user setting) | Parent | child_name, session_summary, progress |
| PAYMENT_RECEIPT | Successful payment | Subscription owner | amount, plan, next_renewal, invoice_url | |
| RENEWAL_FAILED | Stripe webhook: payment_failed | Subscription owner | amount, retry_date, update_payment_url | |
| SUBSCRIPTION_EXPIRING | 7 days before expiry | Subscription owner | expiry_date, renew_url | |
| DATA_EXPORT_READY | GDPR export completed | Requesting user | download_url, expires_in | |
| ACCOUNT_SUSPENDED | Admin suspends account | Suspended user | name, reason, contact_url | |
| CHILD_LOCKED_PIN | 5 failed PIN attempts | In-app (Teacher Portal) | Class teacher | child_name, class_name |
23. Edge Cases and Failure Handling
Source: (D) | Status: Confirmed as spec
23.1 Child Forgot PIN
- Child cannot reset own PIN. No self-service.
- Teacher resets via Teacher Portal β Student row β Reset PIN (flow 19.7)
- New PIN shown once in modal
- Old PIN immediately invalidated (UPDATE students.pin_hash)
23.2 Teacher Forgot Password
- Teacher visits /forgot-password β standard email reset flow (flow 15.5)
- If teacher no longer has email access: contact school admin β admin resets via admin tool (Section 27)
23.3 Parent Wants to Claim Existing Teacher-Created Child
- Flow 20.1 applies
- If child already has a parent_id: "This child already has a parent account. Contact the school."
- Platform admin can transfer parent ownership if proof provided
23.4 Subscription Expires Mid-Term
subscriptions.stateβpast_dueon payment failure- Grace period: 7 days full access
grace_ends_attriggers: state βexpired, children enter free tier- Teacher sees "Subscription expired" banner β upgrade path shown
- All bot cycles stop for expired learners
- Reading sessions still work (free-tier book limits apply)
- Reports frozen β last generated report cached
23.5 Class Deleted with Active Children
- Class state β
archived - Children β
students.state = 'inactive',class_id = null - Historical data retained (telemetry, memories, sessions) under learner_id
- Teacher must re-assign inactive students to new class to reactivate
23.6 Duplicate Email on Registration
- 409 Conflict response
- Show: "An account with this email already exists." + Login link
- Do NOT expose whether account is teacher, parent, or unverified
- If user is unverified: offer "Resend verification email" link
23.7 Duplicate Child Profile (Same Name Imported Twice)
- On CSV import: if name matches existing student in same class β warn in import preview
- Teacher can: skip duplicate OR merge (sets duplicate to archived, migrates data to original)
- Platform admin can merge via admin tools (Section 27)
23.8 Invited User Never Verifies
- Invite token expires after 7 days
- Teacher can re-send invite from invite list (generates new token, invalidates old)
- Max 3 resends before invite marked
failed(admin must investigate)
23.9 School Stops Paying
- School subscription β
past_dueβ 7-day grace βexpired - All school teachers: Teacher Portal access reduced to free tier
- All school students: reading limited to free 50 books
- Bot cycles stop
- 30-day notice: send SCHOOL_SUBSCRIPTION_EXPIRED email with data export link
- After 30 days no renewal: accounts marked for archiving, data export window offered
23.10 Teacher Leaves School
- School admin: Reassign teacher's classes to another teacher
- UPDATE
classes.teacher_id = new_teacher_id - All students remain in class unchanged
- Departing teacher: account set to
suspendedor transferred to personal plan - If no school admin exists: platform admin handles via admin tools
23.11 Child Changes Class
- Use flow 19.10 (Move Child Between Classes)
- All historical data migrated under same learner_id
- Learner Bot continues with existing memory β no restart
- New teacher gets read access to full history
23.12 No Reading Data Yet (New Child, First Login)
- Library shows general age/level recommendations (not personalised)
- "Picked for You" card shows bestseller at estimated grade level
- Learner Bot does NOT run for children with 0 sessions
- Teacher Portal shows student in "No activity yet" state with placement test nudge
23.13 Telemetry Fails but Reading Session Completes
- Client: Service Worker queues missed events in IndexedDB
- Retry: automatic on next network connection (workbox-background-sync)
- If IndexedDB events fail to replay after 72h: events purged (client-side limit)
- Server: session still exists (created at book_opened), marked
completedif session_ended received even without all events - Learner Bot: runs with partial data. Missing events β lower confidence score on analysis (0.60 instead of 0.90)
- No error shown to child
23.14 Placement Test Skipped or Failed Midway
- Child closes app during placement test
placement_test_completedremains false- On next login: placement test resumed from beginning (no partial results stored)
- After 3 abandons: assign default reading_level = 2.0, mark
placement_test_completed = true(with flagplacement_inferred = true) - Teacher notified via TEACHER_ALERT_NO_PLACEMENT
23.15 Assessment Bot Returns Ambiguous Level Recommendation
- If score_pct == exactly 65% (boundary): default to
hold - If score_pct is null (bot error): log error, do not update learner state, retry on next nightly cycle
- If Assessment Bot is unreachable: Learner Bot continues without quiz result, skips mastery update for this cycle
33. Notification Triggers β Full Contracts
Source: (D) | Status: Confirmed as spec. Delivery via SendGrid. MUST BUILD.
| ID | Trigger | Channel | Recipient | Template Variables |
|---|---|---|---|---|
| VERIFY_EMAIL | User registers | Registrant | name, verify_url, expires_in_hours |
|
| WELCOME_TEACHER | Email verified (teacher) | Teacher | name, portal_url |
|
| WELCOME_PARENT | Email verified (parent) | Parent | name, portal_url |
|
| INVITE_EMAIL | Invite created | Invitee | inviter_name, school_name, invite_url, expires_in_days |
|
| PASSWORD_RESET | Forgot password submitted | User | name, reset_url, expires_in_minutes |
|
| PASSWORD_CHANGED | Password reset completed | User | name, changed_at |
|
| ACCOUNT_LOCKED_ALERT | 5 failed login attempts | User | name, unlock_time, support_url |
|
| ACCOUNT_SUSPENDED | Admin suspends account | User | name, reason, contact_url |
|
| SCHOOL_SUBSCRIPTION_EXPIRED | School sub expires | School admin + all teachers | school_name, expired_date, renewal_url, data_export_url |
|
| SUBSCRIPTION_EXPIRED | Individual sub expires | User | name, expired_date, renewal_url |
|
| RENEWAL_FAILED | Stripe payment_failed webhook | Subscription owner | name, amount, retry_date, update_payment_url |
|
| SUBSCRIPTION_EXPIRING | 7 days before expiry | Subscription owner | name, expiry_date, renew_url |
|
| PARENT_CLAIM_APPROVED | Teacher approves claim | Parent | child_name, portal_url |
|
| PARENT_CLAIM_REJECTED | Teacher rejects claim | Parent | child_name, reason |
|
| WEEKLY_PARENT_DIGEST | Every Sunday 08:00 UTC | Parent (full tier) | child_name, week_summary, books_read, miles, highlight, home_tip, portal_url |
|
| CHILD_LOCKED_PIN | 5 failed PIN attempts | In-app (Teacher Portal) | Class teacher | child_name, class_name, reset_url |
| TEACHER_ALERT_LOW_SCORE | Bot: avg quiz <65% on 2+ quizzes | In-app (Teacher Portal) | Class teacher | child_name, avg_score, quiz_count, report_url |
| TEACHER_ALERT_INACTIVE | No sessions in 4+ days | In-app (Teacher Portal) | Class teacher | child_name, last_active_date |
| TEACHER_ALERT_NO_PLACEMENT | Placement test not completed | In-app (Teacher Portal) | Class teacher | child_name |
| PAYMENT_RECEIPT | Stripe invoice.payment_succeeded | Subscription owner | name, amount, plan, next_renewal, invoice_url |
|
| DATA_EXPORT_READY | GDPR export completed | Requesting user | download_url, expires_in_hours |
34. Edge Cases β Full Contracts
Source: (D) | Status: Confirmed as spec
34.1 Parent Claims Existing Teacher-Created Child
Scenario: Teacher created "sofia001". Parent registers and wants to link Sofia.
Flow: Section 31.2 (parent claim). No data conflict β claim only adds visibility.
Edge case: Child already has 2 parents linked.
- Check:
students.parent_count = 2β 409{error: 'max_parents_reached'}. Parent must contact school.
Edge case: Teacher rejects claim.
- Parent receives PARENT_CLAIM_REJECTED email. Child account unchanged. Parent has no recourse except contacting school directly.
34.2 Duplicate Child Profile
Detection: Same name in same class during CSV import β warning flag.
Resolution options:
- Import as new (teacher accepts duplicate β maybe twins).
- Skip duplicate row.
- Admin merge post-import (Flow 30.10).
Never auto-merge. Teacher or admin must explicitly confirm.
34.3 Duplicate Email on Registration
Check: users table by email. If exists:
- state=
activeβ 409{error: 'email_taken', action: 'login'}. Client shows "Already registered. Log in." - state=
pending_verificationβ 409{error: 'pending_verification', action: 'resend'}. Client shows "Check your email." - state=
archivedβ 409{error: 'email_archived'}. Contact support (possible GDPR deletion then re-register β support must handle manually).
34.4 School Subscription Ends Mid-Year
Scenario: School subscription state β expired (payment lapsed).
Sequence:
- Stripe payment_failed β
past_due+ 7-day grace. - Grace period: send RENEWAL_FAILED to school admin. Full access maintained.
- Grace expires:
expired. All teachers β free tier. All students β free tier (50 books). - Learner Bot: stop nightly cycles for all school students.
- 30-day window: school admin can still log in, export data, renew.
- After 30 days no renewal: INSERT
scheduled_archive(school_id, archive_at=now()+30d). - On archive_at: all student accounts β
archived. Data retained per GDPR retention schedule.
Audit Log: Every state transition logged.
34.5 Teacher Leaves School
Scenario: Teacher account needs to be decoupled from school. Students must not be orphaned.
Admin action (school_admin or platform_admin):
GET /api/admin/school/:school_id/teachers/:teacher_id/classesβ list affected classes.PATCH /api/v1/classes/:class_id{teacher_id: new_teacher_id}β reassign each class.- UPDATE
users.school_id=nullfor departing teacher (or transfer to personal plan). - Students remain in classes unchanged.
If no replacement teacher: Classes marked unassigned. Students remain in_class. School admin must re-assign.
Audit Log: Full chain logged.
34.6 Child Changes Class (Mid-Year)
Flow: Section 30.7 (move student).
Data continuity: All historical data follows learner_id. New teacher sees full history.
Learner Bot: On next nightly cycle, Orchestrator picks up new class context (year_level, curriculum_territory may change). Bot does NOT reset memory. Bot notes class change in reading_pattern memory.
Quiz continuity: In-progress assignments from old class: remain accessible, not auto-archived. New teacher can view.
34.7 No Telemetry But Reading Session Completes
Scenario: App offline or telemetry service down.
Client behaviour:
- Service Worker queues all events in IndexedDB.
- On reconnect: workbox-background-sync replays events (up to 72h).
- Session state:
reading_sessionscreated atbook_openedevent. Ifsession_endedreceived (even delayed) β markedcompleted.
Bot behaviour: If session summary fires late (>1h after session ended) β bot still processes. No error.
If events lost entirely (>72h offline): Session remains in state open indefinitely. Cron job (runs every 6h): mark sessions open with last_event_at < now()-24h as abandoned. Bot gets no data for this session.
Audit Log: INSERT audit_log (action='session_force_abandoned', metadata={session_id, reason: 'timeout'})
34.8 Reset Password for Child (Teacher-Issued Credentials)
Children have no email. Password reset means PIN reset.
Flow: Teacher Portal β Student row β Reset PIN (Flow 30.6).
If teacher also unavailable: School admin resets PIN.
If school admin also unavailable: Platform admin resets via admin tools (Section 24.3).
No self-service PIN reset for children β by design.
34.9 Subscription Expires with Active Reading History
What is preserved:
- All telemetry events (permanent, GDPR retention applies)
- All Learner Bot memories
- All reading sessions
- All quiz results
- Reading level history
- Vocabulary gaps
What is locked:
- Full library access (β 50 books)
- AI reports (last cached report visible, labelled "Subscription required for new reports")
- Learner Bot nightly cycle (stopped)
- Comprehension quiz (locked)
What is NOT deleted: Nothing is deleted on expiry. Data only deleted on GDPR request or account archival.
Teacher Portal: Shows "Subscription expired" banner on every page. Renew link displayed.
34.10 Invited User Never Verifies
Token expires after 7 days (for invites) or 48h (for self-registration).
Invite: Teacher sees invite in list with status pending. "Resend" button available.
- Max 3 resends. On 3rd failure β invite marked
failed. Teacher must create a new invite. usersrow created at accept-invite step only, not at invite-send step. So no zombie users.
Self-registration (teacher/parent): User in pending_verification state.
- Resend: up to 3 times per hour.
- After 30 days pending_verification with no action: cron marks account
archived. INSERTaudit_log.
34.11 Entitlement Check Failure (Service Down)
Scenario: Account Center / billing endpoint unreachable when child tries to read.
Behaviour: checkEntitlement() catches timeout. Default fallback: {tier: 'free'}.
Rationale: Fail closed on entitlement β free tier access maintained. Never block reading entirely due to billing service outage.
Audit Log: INSERT audit_log (action='entitlement_check_failed', metadata={error, fallback: 'free'})