Skip to content

[EPAC-917]: Daily Parliament digest notification (iOS)#805

Merged
riddim-developer-bot[bot] merged 1 commit into
mainfrom
symphony/epac-917-daily-parliament-digest-notification-what-happen
Jun 14, 2026
Merged

[EPAC-917]: Daily Parliament digest notification (iOS)#805
riddim-developer-bot[bot] merged 1 commit into
mainfrom
symphony/epac-917-daily-parliament-digest-notification-what-happen

Conversation

@riddim-developer-bot

Copy link
Copy Markdown
Contributor

Summary

Adds an opt-in once-per-sitting-day local notification that summarizes the day's parliamentary activity (subject count, top three subjects, attendance estimate, and one optional vote summary). Content is composed from authoritative Hansard subjects and recorded-division results only — no generated or paraphrased text. Linear: EPAC-917.

Behavior

  • Fires within iOS background refresh after today's Hansard ingests, and opportunistically on app foreground (scenePhase .active).
  • Only on confirmed sitting days (Hansard published, not just scheduled), between 17:00 and 23:00 local time, once per calendar day.
  • Tapping opens cabinetdoor://sitting/<yyyy-MM-dd>, routing the user to today's sitting in the Debates calendar via the existing deep-link router.
  • Default off. Opt in from onboarding screen 4 ("Daily Parliament summary") or Settings → Notifications → Daily Parliament summary.

Architecture

Clean Architecture shape exactly as named in the issue.

  • Use case: SendDailyParliamentDigest composes a DailyDigest value object from four read ports and hands it to DigestNotificationPort. The use case owns timing policy (window + sitting-day + dedupe); adapters are pure plumbing.
  • Entities (ios/epac/Domain/Entities/): DailyDigest; NotificationPreferences (renamed from User to avoid a collision with ExyteChat.User in the same module).
  • Ports (ios/epac/Domain/Ports/):
    • HansardReadPort — sitting-day, subject count, top subjects
    • VoteReadPort — attendance estimate, daily vote summary
    • DigestNotificationPort — UNUserNotificationCenter delivery
    • UserPreferenceReadPort — daily-digest opt-in flag
    • DigestDeliveryRecordPort — last-delivered day (dedupe)
  • Adapters (ios/epac/Data/Adapters/):
    • SwiftDataHansardReadAdapter — reads Hansard / OrderOfBusiness / SubjectOfBusiness
    • SwiftDataVoteReadAdapter — averages (yea+nay+paired)/338 across the day's RecordedVote rows for attendance; first vote of the day for the summary
    • LiveDigestNotificationAdapter — schedules a local notification with userInfo[date] for the tap handler
    • UserDefaultsDigestDeliveryRecordAdapter — once-per-day dedupe key
    • UserPreferenceAdapter — opt-in flag (UserDefaults)
  • Composition root: BackgroundRefreshManager.sendDailyDigestIfEligible(container:) wires the use case after Fetch.backgroundRefresh() (which now also downloads today's Hansard) and is invoked on scenePhase .active.
  • Tap deep link: AppDelegate adopts UNUserNotificationCenterDelegate; didReceive parses userInfo[date] and calls UIApplication.shared.open on cabinetdoor://sitting/<yyyy-MM-dd>, which the existing ContentView deep-link router routes to the Parliament tab.

docs/architecture/use-case-catalog.md is updated with the use case, entities, and ports.

Acceptance criteria

  • Fires within 60 minutes of full Hansard ingestion on sitting days — wired into background refresh after Fetch.downloadHansard(Date()) succeeds, with the use case re-checking eligibility.
  • Content: subject count, attendance estimate (from vote records when available), top 3 subjects, vote result if any — all from the official record.
  • Sent only on confirmed sitting days — HansardReadPort.isSittingDay returns true only when Hansard exists for that date.
  • Opt-in toggle in onboarding screen 4 and in Settings → Notifications.
  • Default off; user must explicitly enable.
  • Notification taps to today's sitting in the Debates calendar via cabinetdoor://sitting/<yyyy-MM-dd>.
  • PR linked in this ticket (after creation).

Test plan

  • xcodebuild build on iPhone 17 Pro (iOS 26.5) simulator — BUILD SUCCEEDED
  • SendDailyParliamentDigestTests — 9 cases: opt-in, start/end-hour boundaries (minute precision), non-sitting day, dedupe, top-subjects limit propagation. All pass.
  • DailyDigestFormatterTests — 6 cases for title/body composition. All pass.
  • SnapshotTests.testNotificationsSettings_dailyDigest (light/dark/a11y) — passes.
  • SnapshotTests.testSettings_root snapshots regenerated for the new Notifications row — passes.
  • swiftlint --strict on changed files — zero violations.
  • Live verification on a real sitting day with simulator notifications enabled — captured under the Project's Human Handoff issue per the issue's external-validation note (gates: none).

Boundary notes

  • The use case never imports UserNotifications, SwiftData, or any UI framework.
  • All displayed text is composed from typed values by DailyDigestFormatter (framework-free, verbatim authoritative content).
  • French strings added alongside English in Localizable.strings.

🤖 Generated with Claude Code

Adds an opt-in once-per-sitting-day local notification that summarizes
the day's parliamentary activity for users who want a general "what
happened today" feed instead of topic-specific alerts. Content is
composed from authoritative Hansard subjects and recorded-division
results only — no generated or paraphrased text.

Behavior
--------
- Sends within iOS background refresh after today's Hansard ingests, and
  opportunistically on app foreground.
- Fires only on confirmed sitting days (Hansard published, not just
  scheduled), between 17:00 and 23:00 local, once per calendar day.
- Tapping opens the cabinetdoor://sitting/<date> deep link, routing the
  user to today's sitting in the Debates calendar.
- Default OFF. Opt in from onboarding screen 4 ("Daily Parliament
  summary") or from Settings → Notifications → Daily Parliament summary.

Implementation
--------------
Use case: `SendDailyParliamentDigest` composes a `DailyDigest` value
object from four read ports and hands it to `DigestNotificationPort`.
The use case owns the timing policy (window + sitting-day + dedupe);
adapters are pure plumbing.

Ports (all `outbound`, all in `ios/epac/Domain/Ports/`):
- `HansardReadPort` — sitting-day, subject count, top subjects
- `VoteReadPort` — attendance estimate, daily vote summary
- `DigestNotificationPort` — UNUserNotificationCenter delivery
- `UserPreferenceReadPort` — daily-digest opt-in flag
- `DigestDeliveryRecordPort` — last delivered day, for dedupe

Adapters (`ios/epac/Data/Adapters/`):
- `SwiftDataHansardReadAdapter` — reads Hansard + OrderOfBusiness +
  SubjectOfBusiness from the local SwiftData store
- `SwiftDataVoteReadAdapter` — averages (yea+nay+paired)/338 across
  the day's RecordedVote rows for attendance; first vote of the day
  for the summary
- `LiveDigestNotificationAdapter` — schedules a local notification via
  UNUserNotificationCenter with `userInfo` containing the date string
  for the tap handler
- `UserDefaultsDigestDeliveryRecordAdapter` — once-per-day dedupe key
- `UserPreferenceAdapter` — opt-in flag in UserDefaults

Composition: `BackgroundRefreshManager.sendDailyDigestIfEligible(container:)`
wires the use case after `Fetch.backgroundRefresh()` (which now downloads
today's Hansard) and is also invoked on scenePhase .active.

Tap deep link: `AppDelegate` becomes the
`UNUserNotificationCenterDelegate`; `didReceive` parses
`userInfo[date]` and calls `UIApplication.shared.open` on
`cabinetdoor://sitting/<yyyy-MM-dd>`, which the existing `ContentView`
deep-link router routes to the Parliament tab.

Entities (`ios/epac/Domain/Entities/`):
- `DailyDigest` — date, subjectCount, attendanceEstimate?, topSubjects,
  vote? (VoteSummary: billName, passed, yeas, nays)
- `NotificationPreferences` — local opt-in flags (renamed from `User`
  to avoid a collision with `ExyteChat.User` in the same module)

Presentation:
- `OnboardingView` gains a `dailyDigestOptInRow` toggle on the Topics
  screen; opt-in persists to UserDefaults on completion.
- New `NotificationsSettingsView` exposes the same toggle and is
  reachable from Settings → Notifications.

Tests:
- `SendDailyParliamentDigestTests` — 9 cases covering opt-in,
  start/end-hour boundaries (minute precision), non-sitting day,
  dedupe, top-subjects limit propagation.
- `DailyDigestFormatterTests` — 6 cases for the title/body composition.
- `SnapshotTests.testNotificationsSettings_dailyDigest` — light, dark,
  a11y for the new settings screen.
- `Settings_root` snapshots regenerated (new Notifications row).

Catalog: `SendDailyParliamentDigest`, the five ports, and the
`DailyDigest`/`NotificationPreferences` entities added to
`docs/architecture/use-case-catalog.md`.

Verification
------------
- xcodebuild build: BUILD SUCCEEDED (Xcode 26.5 / iPhone 17 Pro sim)
- xcodebuild test (use case + formatter): 15 tests, all passed
- Settings snapshot tests: passed after baseline regen
- swiftlint --strict: zero new violations in changed files

Release-Note: Daily Parliament summary notification (opt-in) on sitting days
@riddim-developer-bot
riddim-developer-bot Bot enabled auto-merge (squash) June 14, 2026 09:28
@riddim-developer-bot
riddim-developer-bot Bot merged commit e706b08 into main Jun 14, 2026
35 of 36 checks passed
@riddim-developer-bot
riddim-developer-bot Bot deleted the symphony/epac-917-daily-parliament-digest-notification-what-happen branch June 14, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants