Skip to content

Commit 68aa759

Browse files
thewatermethodCopilotCopilot
authored
[TTAHUB-5539] Specification for actionable notification backend (#3661)
* Add actionble notification specification * Remove dead link * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Update index.md * Update index.md * Update index.md * Update spec * Add new subsection * Update spec * Correct spelling mistake * Update spec * Update delete spec * [TTAHUB-5383] Add notification model (#3665) * feat: add Notifications schema (model, migrations, seed, tests) - Add NOTIFICATION_TYPES and actionable_notifications feature flag to constants - Create Notifications table migration with all spec columns including archivedAt/viewedAt (DATEONLY) - Add actionable_notifications to enum_Users_flags migration - Create Notification Sequelize model with User association - Add User.hasMany(Notification) association - Add seed data with user-specific and global notifications - Add model tests (8 passing) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify model * Update to add "triggeredAt" to model * Add all enum types * Remove digest from notifications enum * Update model with other needed field --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * [TTAHUB-5385] Add notification scopes (#3668) * feat: add Notifications schema (model, migrations, seed, tests) - Add NOTIFICATION_TYPES and actionable_notifications feature flag to constants - Create Notifications table migration with all spec columns including archivedAt/viewedAt (DATEONLY) - Add actionable_notifications to enum_Users_flags migration - Create Notification Sequelize model with User association - Add User.hasMany(Notification) association - Add seed data with user-specific and global notifications - Add model tests (8 passing) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify model * Update to add "triggeredAt" to model * Add all enum types * Scopes, work in progress * Remove digest from notifications enum * Add notification scopes * Register scopes * Fix bugs, add integration test * Update model with other needed field --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename migration * Rename seeder * Add feature flag migration to notifications table * Fix LDM + missed import * [TTAHUB-5384] Add notification services (#3673) * feat: add Notifications schema (model, migrations, seed, tests) - Add NOTIFICATION_TYPES and actionable_notifications feature flag to constants - Create Notifications table migration with all spec columns including archivedAt/viewedAt (DATEONLY) - Add actionable_notifications to enum_Users_flags migration - Create Notification Sequelize model with User association - Add User.hasMany(Notification) association - Add seed data with user-specific and global notifications - Add model tests (8 passing) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Simplify model * Update to add "triggeredAt" to model * Add all enum types * Scopes, work in progress * Remove digest from notifications enum * Add notification scopes * Register scopes * Fix bugs, add integration test * Add services and tests * Update model with other needed field * Accomodate missing column in services * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Add notification configuration tests * fix: replace generic deleteNotification(scopes) with targeted delete helpers - deleteNotification(notificationId) now accepts a single ID, throws when falsy — eliminates the empty-scopes full-table-delete risk - adds deleteNotificationsByEntityAndType(entityId, notificationType) for event-driven stale-notification cleanup; throws when either arg is falsy - updates and expands tests to cover both new signatures and their guards - updates spec doc with typed signatures and safety-guard notes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix bad test * [TTAHUB-5387] Add notification handlers (#3674) * fix: cap getNotifications limit at 100 and add comprehensive tests - Fix Math.max -> Math.min bug in getNotifications limit calculation - Add handler unit tests (src/routes/notifications/handlers.test.ts) - Add policy unit tests (src/policies/notifications.test.ts) - Fix and expand service tests for sort field/direction validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add param middleware * Updates from code review * refactor: separate per-user notification state into NotificationUserStates table The Notifications table previously stored viewedAt and archivedAt directly on the notification row. Global notifications (userId=null) could not track whether different users had independently viewed or archived them — a single row cannot hold state for multiple users. Changes: - Migration: creates NotificationUserStates (notificationId, userId, viewedAt, archivedAt, UNIQUE on notificationId+userId with FK cascades), backfills existing per-user state, and drops viewedAt/archivedAt from Notifications - New model: NotificationUserState with belongsTo Notification+User - Notification model: removed archivedAt/viewedAt/isInformational, added hasMany(NotificationUserState, { as: 'userStates' }) - Service: updateNotification → updateNotificationState(notificationId, userId, { viewedAt?, archivedAt? }) — upserts per-user state row; getNotifications(userId, scopes, options) — LEFT JOINs state, filters archived, returns NotificationWithState[] - Types: added NotificationUserStateModel and NotificationWithState - Policy: added isGlobalNotification(); canUpdateNotification() now allows admin, owner, or global notification (any user can set their own state) - Handlers: getNotificationsHandler passes userId as first arg; updateNotificationHandler calls updateNotificationState - Spec: updated to document new table, service signatures, cleanup logic - Tests: 121/121 passing across 10 suites Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update to remove findOrCreate * Update seeder * Define relation in both directions * Update test * Map scopes to types directly to prevent drift * Updates from code review --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Regenerate Logical Data Model * Condense down notifications table * Remove extraneous migration * Fix LDM bugs * Remove extra columns from migration --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 37d116b commit 68aa759

33 files changed

Lines changed: 3460 additions & 65 deletions

docs/logical_data_model.encoded

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

docs/logical_data_model.puml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,30 @@ class NextSteps{
10571057
completeDate : date
10581058
}
10591059

1060+
class NotificationUserStates{
1061+
* id : integer : <generated>
1062+
* notificationId : integer : REFERENCES "Notifications".id
1063+
* userId : integer : REFERENCES "Users".id
1064+
* createdAt : timestamp with time zone
1065+
* updatedAt : timestamp with time zone
1066+
archivedAt : date
1067+
viewedAt : date
1068+
}
1069+
1070+
class Notifications{
1071+
* id : integer : <generated>
1072+
userId : integer : REFERENCES "Users".id
1073+
* createdAt : timestamp with time zone
1074+
* type : enum
1075+
* updatedAt : timestamp with time zone
1076+
displayId : varchar(255)
1077+
entityId : integer
1078+
label : text
1079+
link : text
1080+
text : text
1081+
triggeredAt : date
1082+
}
1083+
10601084
class ObjectiveCollaborators{
10611085
* id : integer : <generated>
10621086
* collaboratorTypeId : integer : REFERENCES "CollaboratorTypes".id
@@ -1350,6 +1374,7 @@ class Users{
13501374
}
13511375

13521376
enum enum_Users_flags {
1377+
actionable_notifications
13531378
monitoring-regional-dashboard
13541379
quality_assurance_dashboard
13551380
}
@@ -2524,6 +2549,34 @@ class ZALNextSteps{
25242549
session_sig : text
25252550
}
25262551

2552+
class ZALNotificationUserStates{
2553+
* id : bigint : <generated>
2554+
* data_id : bigint
2555+
* dml_as : bigint
2556+
* dml_by : bigint
2557+
* dml_timestamp : timestamp with time zone
2558+
* dml_txid : uuid
2559+
* dml_type : enum
2560+
descriptor_id : integer
2561+
new_row_data : jsonb
2562+
old_row_data : jsonb
2563+
session_sig : text
2564+
}
2565+
2566+
class ZALNotifications{
2567+
* id : bigint : <generated>
2568+
* data_id : bigint
2569+
* dml_as : bigint
2570+
* dml_by : bigint
2571+
* dml_timestamp : timestamp with time zone
2572+
* dml_txid : uuid
2573+
* dml_type : enum
2574+
descriptor_id : integer
2575+
new_row_data : jsonb
2576+
old_row_data : jsonb
2577+
session_sig : text
2578+
}
2579+
25272580
class ZALObjectiveCollaborators{
25282581
* id : bigint : <generated>
25292582
* data_id : bigint
@@ -3038,6 +3091,7 @@ NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" EventReportPilotNation
30383091
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenterUsers : nationalCenter, nationalCenterUsers
30393092
NationalCenters "1" --[#black,dashed,thickness=2]--{ "n" NationalCenters : mapsToNationalCenter, mapsFromNationalCenters
30403093
NextSteps "1" --[#black,dashed,thickness=2]--{ "n" NextStepResources : nextStep, nextStepResources
3094+
Notifications "1" --[#black,dashed,thickness=2]--{ "n" NotificationUserStates : notification, userStates
30413095
ObjectiveTemplates "1" --[#black,dashed,thickness=2]--{ "n" GoalTemplateObjectiveTemplates : objectiveTemplate, goalTemplateObjectiveTemplates
30423096
ObjectiveTemplates "1" --[#black,dashed,thickness=2]--{ "n" Objectives : objectives, objectiveTemplate
30433097
Objectives "1" --[#black,dashed,thickness=2]--{ "n" ActivityReportObjectives : objective, originalObjective, activityReportObjectives, reassignedActivityReportObjectives
@@ -3087,6 +3141,8 @@ Users "1" --[#black,dashed,thickness=2]--{ "n" GoalCollaborators : user, goalCo
30873141
Users "1" --[#black,dashed,thickness=2]--{ "n" GoalStatusChanges : user, goalStatusChanges
30883142
Users "1" --[#black,dashed,thickness=2]--{ "n" GroupCollaborators : user, groupCollaborators
30893143
Users "1" --[#black,dashed,thickness=2]--{ "n" NationalCenterUsers : user, nationalCenterUsers
3144+
Users "1" --[#black,dashed,thickness=2]--{ "n" NotificationUserStates : user, notificationUserStates
3145+
Users "1" --[#black,dashed,thickness=2]--{ "n" Notifications : user, notifications
30903146
Users "1" --[#black,dashed,thickness=2]--{ "n" ObjectiveCollaborators : user, objectiveCollaborators
30913147
Users "1" --[#black,dashed,thickness=2]--{ "n" Permissions : user, permissions
30923148
Users "1" --[#black,dashed,thickness=2]--{ "n" SessionReportPilotTrainers : trainer, sessionTrainers
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# Actionable Notifications
2+
3+
_Technical design/specification for review and implementation._
4+
5+
The actionable notification epic encompasses both several new email notifications and an entirely new system of in-app notifications. In-app notificatiions can be triggered by a variety of different events in lifecycles of disparate entities (Activity Reports, Training Reports, Collab Reports)
6+
7+
[Prototype is available in Figma](https://www.figma.com/design/LNF1ux5pEABIOD10T2oBUP/Actionable-Notifications?node-id=1328-11078&p=f&m=dev)
8+
9+
This document is meant to be a living record of specifications and decisions while the feature is being developed. This document will be used to synthesize documentation within this repo as a final task in the epic, and then can be deleted.
10+
11+
## Requirements (from Jira)
12+
13+
- The solution should be scalable. The solution should be able to accomodate the addition of new notification types over time.
14+
- The solution should account for both in app and email notifications.
15+
- The solution should account for user opt ins and outs.
16+
- The solution should account for both one off emails and digest emails.
17+
18+
## Principles for reference
19+
20+
- Avoid the use of hooks to create notifications. Doing it inline (for example, right when changes are requested to an activity report rather than a hook that requests to a status change) makes it traceable and reduces the need to query the database again for associated metadata in a new context
21+
- Use Typescript throughout, strictly
22+
- Validate requests with Joi
23+
24+
## Steps
25+
26+
### Database Tables/Sequelize Models
27+
28+
**Ticket #1: [Create Notifications schema](https://jira.acf.gov/browse/TTAHUB-5383)**
29+
Points: 5
30+
31+
#### Notifications
32+
33+
- userId: FK to users, nullable
34+
- entityId: ID of the source entity, nullable (polymorphic; database FK enforcement is not possible unless the schema uses separate typed columns/tables)
35+
Potential links are: group, communicationLog, activityReport, collabReport, trainingReport, sessionReport
36+
https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
37+
- type: NOTIFICATION_TYPE[enum]
38+
- link: computed link
39+
- label: label for link
40+
- displayId: displayed in the second column of the UI; it's a whole report ID like `R01-AR-1234`
41+
- text: computed message (see notification configuration, next section)
42+
- triggeredAt: Date, nullable
43+
44+
```timestamps: true```
45+
```paranoid: false```
46+
47+
#### NotificationUserStates
48+
49+
- notificationId: FK to Notifications, NOT NULL, ON DELETE CASCADE
50+
- userId: FK to Users, NOT NULL
51+
- viewedAt: DATEONLY, nullable — per-user view timestamp
52+
- archivedAt: DATEONLY, nullable — per-user archive timestamp
53+
- UNIQUE constraint on `(notificationId, userId)`
54+
- Rationale: separates notification content from per-user interaction state; enables global notifications (`Notifications.userId IS NULL`) to track independent read/archive state per user
55+
56+
Create a migration with these database tables, and a short-lived feature flag (`actionable_notifications`)to use during development.
57+
Create simple seeded data, also for use during development
58+
59+
### Notification configuration
60+
61+
The full enum lives in [`src/constants.js`](../../src/constants.js) under `NOTIFICATION_TYPES`. The table below maps each notification trigger (by CSV row ID) to its enum key. See [`notifications.csv`](./notifications.csv) for full copy, recipient, and channel detail.
62+
63+
**Convention:** one enum key per *trigger*. When the same event fans out to multiple recipients (e.g., approver, creator, collaborator), those variants share a single key — the recipient is resolved at send time via metadata passed to `createNotification`.
64+
65+
**Excluded rows:** rows with status `Out of scope` in the CSV are not represented in the enum. Rows with status `Paused` are included and noted below.
66+
67+
**Worked example:**
68+
69+
```ts
70+
// src/constants.js
71+
const NOTIFICATION_TYPES = {
72+
ACTIVITY_REPORT_NEEDS_ACTION: 'changesRequested',
73+
// ... etc
74+
};
75+
76+
// Paired NOTIFICATION_CONFIGURATION entry (to be added per notification)
77+
const NOTIFICATION_CONFIGURATION = {
78+
[NOTIFICATION_TYPES.ACTIVITY_REPORT_NEEDS_ACTION]: {
79+
textFn: ({ userName, recipientName }) =>
80+
`${userName} has requested changes to your Activity Report for ${recipientName}.`,
81+
// whether or not we display primary button style or outline button style ("view" vs "take action")
82+
actionable: true,
83+
linkFn: ({ id }) => `/activity-reports/${id}`,
84+
linkText: (metadata) => 'View AR',
85+
displayId: (metadata) => `${metadata.displayId}`,
86+
},
87+
};
88+
```
89+
90+
#### Notification inventory
91+
92+
##### Activity Report
93+
94+
| CSV row(s) | Enum key | Notes |
95+
|---|---|---|
96+
| AR-1a/b | `ACTIVITY_REPORT_COLLABORATOR_ADDED` | existing |
97+
| AR-2a–d, AR-3a–d | `ACTIVITY_REPORT_SUBMITTED` | existing; covers both creator & collaborator submitting |
98+
| AR-4a–d, AR-5a–d | `ACTIVITY_REPORT_RESUBMITTED` | new |
99+
| AR-6a–f, AR-8a–f | `ACTIVITY_REPORT_NEEDS_ACTION` | existing; covers Approver 1 & 2 requesting changes |
100+
| AR-7a–f, AR-9a–f | `ACTIVITY_REPORT_APPROVED` | existing; covers Approver 1 & 2 approvals |
101+
| `ACTIVITY_REPORT_RECIPIENT_REPORT_APPROVED` | recipient notified on final approval | existing |
102+
| AR-10a | `ACTIVITY_REPORT_SUBMITTED_DIGEST` | existing |
103+
| AR-11 | `ACTIVITY_REPORT_NEEDS_ACTION_DIGEST` | existing |
104+
| AR-12 | `ACTIVITY_REPORT_APPROVED_DIGEST` | existing |
105+
| AR-13 | `ACTIVITY_REPORT_COLLABORATOR_DIGEST` | existing |
106+
| `ACTIVITY_REPORT_RECIPIENT_REPORT_APPROVED_DIGEST` | recipient digest | existing |
107+
| AR-14 | `ACTIVITY_REPORT_SUBMITTED_TO_COLLABORATOR_DIGEST` | new |
108+
| AR-15 | `ACTIVITY_REPORT_COLLABORATOR_SUBMITTED_DIGEST` | new |
109+
| AR-20 to AR-25 | _(out of scope — not in enum)_ | |
110+
111+
##### Collaborative Report
112+
113+
| CSV row(s) | Enum key | Notes |
114+
|---|---|---|
115+
| CR-1a/b | `COLLAB_REPORT_COLLABORATOR_ADDED` | new |
116+
| CR-2a–d, CR-3a–d | `COLLAB_REPORT_SUBMITTED` | new |
117+
| CR-4a–d, CR-5a–d | `COLLAB_REPORT_RESUBMITTED` | new |
118+
| CR-6a–f, CR-8a–f | `COLLAB_REPORT_NEEDS_ACTION` | new |
119+
| CR-7a–f, CR-9a–f | `COLLAB_REPORT_APPROVED` | new |
120+
| CR-10a | `COLLAB_REPORT_SUBMITTED_DIGEST` | new |
121+
| CR-11, CR-14 | `COLLAB_REPORT_NEEDS_ACTION_DIGEST` | new |
122+
| CR-12, CR-15 | `COLLAB_REPORT_APPROVED_DIGEST` | new |
123+
| CR-13 | `COLLAB_REPORT_COLLABORATOR_DIGEST` | new |
124+
| CR-16 | `COLLAB_REPORT_SUBMITTED_TO_COLLABORATOR_DIGEST` | new |
125+
| CR-17 | `COLLAB_REPORT_COLLABORATOR_SUBMITTED_DIGEST` | new |
126+
127+
##### Training Report
128+
129+
| CSV row(s) | Enum key | Notes |
130+
|---|---|---|
131+
| TR-1a/b | `TRAINING_REPORT_POC_ADDED` | new |
132+
| TR-2a/b | `TRAINING_REPORT_COLLABORATOR_ADDED` | existing |
133+
| TR-3a–e | `TRAINING_REPORT_SESSION_SUBMITTED` | new |
134+
| TR-4a–f | `TRAINING_REPORT_SESSION_NEEDS_ACTION` | new |
135+
| TR-5a–d, TR-6a–d | `TRAINING_REPORT_SESSION_RESUBMITTED` | new |
136+
| _(existing)_ | `TRAINING_REPORT_SESSION_CREATED` | existing |
137+
| _(existing)_ | `TRAINING_REPORT_EVENT_COMPLETED` | existing |
138+
| _(existing)_ | `TRAINING_REPORT_TASK_DUE` | existing; cron umbrella |
139+
| _(existing)_ | `TRAINING_REPORT_EVENT_IMPORTED` | existing |
140+
| TR-5b, TR-7b _(Paused)_ | `TRAINING_REPORT_EVENT_INFO_MISSING` | new; Paused |
141+
| TR-6a, TR-8a _(Paused)_ | `TRAINING_REPORT_EVENT_INFO_PAST_DUE` | new; Paused |
142+
| TR-9a/b, TR-10a/b, TR-11a/b _(Paused)_ | `TRAINING_REPORT_SESSION_INFO_MISSING` | new; Paused |
143+
| TR-10c, TR-12 _(Paused)_ | `TRAINING_REPORT_SESSION_INFO_PAST_DUE` | new; Paused |
144+
| TR-13, TR-15 _(Paused)_ | `TRAINING_REPORT_NO_SESSIONS_CREATED` | new; Paused |
145+
| TR-14, TR-16 _(Paused)_ | `TRAINING_REPORT_NO_SESSIONS_PAST_DUE` | new; Paused |
146+
| TR-17 _(Paused)_ | `TRAINING_REPORT_EVENT_NOT_COMPLETED` | new; Paused |
147+
| TR-18 _(Paused)_ | `TRAINING_REPORT_EVENT_NOT_COMPLETED_PAST_DUE` | new; Paused |
148+
| TR-19 | `TRAINING_REPORT_POC_ADDED_DIGEST` | new |
149+
| TR-20 | `TRAINING_REPORT_COLLABORATOR_ADDED_DIGEST` | new |
150+
| TR-21 | `TRAINING_REPORT_SESSION_SUBMITTED_DIGEST` | new |
151+
| TR-22 | `TRAINING_REPORT_SESSION_NEEDS_ACTION_DIGEST` | new |
152+
| TR-23 | `TRAINING_REPORT_EVENT_INFO_MISSING_DIGEST` | new |
153+
| TR-24, TR-25 | `TRAINING_REPORT_SESSION_INFO_MISSING_DIGEST` | new |
154+
| TR-26 | `TRAINING_REPORT_NO_SESSIONS_CREATED_DIGEST` | new |
155+
| TR-27 | `TRAINING_REPORT_EVENT_NOT_COMPLETED_DIGEST` | new |
156+
157+
##### Communication Log
158+
159+
| CSV row(s) | Enum key | Notes |
160+
|---|---|---|
161+
| CL-1a/b | `COMMUNICATION_LOG_TTA_STAFF_ADDED` | new |
162+
| CL-2a/b | `COMMUNICATION_LOG_RECIPIENT_IN_GROUP` | new |
163+
| CL-3 | `COMMUNICATION_LOG_TTA_STAFF_ADDED_DIGEST` | new |
164+
| CL-4 | `COMMUNICATION_LOG_RECIPIENT_IN_GROUP_DIGEST` | new |
165+
166+
##### Monitoring / Group / System
167+
168+
| CSV row(s) | Enum key | Notes |
169+
|---|---|---|
170+
| Misc-1a/b (Draft) | `MONITORING_GOAL_ADDED` | new; Draft status |
171+
| Misc-1a/b (monitoring data) | `MONITORING_DATA_RECEIVED` | new |
172+
| Misc-2a/b | `GROUP_CO_OWNER_ADDED` | new |
173+
| Misc-3a/b | `GROUP_SHARED` | new |
174+
| Misc-4a/b | `SYSTEM_PLANNED_OUTAGE` | new |
175+
| Misc-5a | `SYSTEM_UNPLANNED_OUTAGE` | new |
176+
177+
### Services
178+
179+
**Ticket #2: [Create Notifications service](https://jira.acf.gov/browse/TTAHUB-5384)**
180+
Points: 8
181+
182+
```createNotification(userId, entityId, NOTIFICATION_TYPE, { metadata })```
183+
184+
Accepts a userId, an entityId (ex: reportId), a notification type, and metadata containing params intended to compute text from the enum. The function should:
185+
- check user for notification preferences
186+
- create a notification
187+
- If possible: Typescript should enforce the parameters match the expected given a NOTIFICATION_TYPE
188+
189+
Ideally, this function should be **plug and play**. See Registering a new notification, below.
190+
191+
```createGlobalNotification```
192+
193+
```updateNotificationState(notificationId, userId, { viewedAt, archivedAt })```
194+
Upserts into `NotificationUserStates` for the given `(notificationId, userId)` pair. Only _viewedAt_ and _archivedAt_ can be updated; this should be enforced in the service, the joi validation, and the model configuration if possible.
195+
196+
197+
```deleteNotification(notificationId: number)```
198+
Deletes a single notification by ID. Throws if `notificationId` is falsy.
199+
Should not be called via handlers — use programmatically only (e.g. the scheduled cleanup job in Ticket #6).
200+
201+
```deleteNotificationsByEntityAndType(entityId: number, notificationType: NotificationType)```
202+
Deletes all notifications for a given entity and type. Used to invalidate stale notifications when a state change makes them no longer actionable (see [Notification Lifecycle](#notification-lifecycle--stale-notification-cleanup), below).
203+
Throws if either `entityId` or `notificationType` is falsy.
204+
Should not be called via handlers — call it inline in the same service function that performs the state change.
205+
206+
```getNotifications(scopes)```
207+
Retrieve all notifications for given scopes. Includes pagination and sorting, offset based, consistent with the rest of the site. LEFT JOIN `NotificationUserStates` so the response returns per-user _viewedAt_ and _archivedAt_ state alongside notification content.
208+
209+
### Scopes
210+
211+
**Ticket #3: [Create required scopes for Notifications](https://jira.acf.gov/browse/TTAHUB-5385)**
212+
Points: 5
213+
214+
- userId
215+
- createdAt
216+
217+
{ userId: nulll } equivalent to "isGlobal" - retrieves all notifications with no user ID
218+
219+
**Ticket #4: [Additional scope: notification type](TTAHUB-5386)**
220+
Points: 3
221+
222+
### Handlers
223+
224+
**Ticket #5: [Handlers for Notifications](https://jira.acf.gov/browse/TTAHUB-5387)**
225+
Points: 3
226+
227+
Create user level notifications and delete DO not need handlers. They will only be called programmatically. Will need a handler with permissions and validation for updating and getting notifications. Users should only be able to update and view their own notifications. Admins will be able to create global notifications.
228+
229+
### Scheduled tasks
230+
231+
**Ticket #6: [Create job to cleanup notifications over thirty days](https://jira.acf.gov/browse/TTAHUB-5389)**
232+
Points: 3
233+
234+
1. Runs every night
235+
2. getNotifications with a createdAt scope < LAST_THIRTY_DAYS & userId != null, joined to `NotificationUserStates`, with `NotificationUserStates.archivedAt IS NOT NULL`
236+
3. Delete notifications
237+
238+
### Notification Lifecycle / Stale Notification Cleanup
239+
240+
Ticket #6 handles **age-based** cleanup (delete all non-global notifications older than 30 days). Notifications will sometimes become invalid via **event-driven invalidation**. As the notifications are created, this will need to be part of the service insertion.
241+
242+
#### Principle
243+
244+
Stale notification cleanup follows the same inline principle as notification creation: call `deleteNotificationsByEntityAndType` inside the same service function that performs the state change. Do not use hooks.
245+
246+
#### Known scenarios requiring cleanup
247+
248+
| Triggering state change | Stale notification type(s) to delete |
249+
|---|---|
250+
| Activity Report returned to "needs action" (un-submitted) | All pending approval-request notifications for that report's approvers |
251+
| Approver removed from an Activity Report | That approver's pending approval-request notifications for that report |
252+
253+
Additional scenarios should be identified and documented here as each notification type is implemented. When in doubt, ask: _"If this event fires, is there a previously-sent notification that no longer makes sense to act on?"_
254+
255+
#### Example (Activity Report un-submit)
256+
257+
```ts
258+
// Inside the service function that handles returning a report to needs-action
259+
await deleteNotificationsByEntityAndType(reportId, NOTIFICATION_TYPES.ACTIVITY_REPORT_APPROVAL_REQUESTED);
260+
// ... then update the report status
261+
```
262+
263+
### Creating a new notification
264+
265+
The process of registering a new notification on the backend: register a new type in the `enum`s. Add `createNotification` into the appropriate service or handler (varies based on specifics of notification). Be sure to pass all metadata necessary (Typescript, validation can help with that)
266+
267+
### Emails
268+
**Ticket #7: [DRY up and simplify existing code for adding a new email notification](TTAHUB-5390)**
269+
Points: 3
270+
271+
Make registering a new email/digest simpler since we'll be adding many more
272+
273+
### Frontend tickets
274+
275+
To be expanded upon in conjunction with PM
276+
277+
- Refactor home page to use new tiled markup
278+
- Create notifications page/table, less filter component
279+
- Filter component, filters
280+
- Create notifications preference page
281+
- Move email opt-in
282+
- Modify site header to add bell component/update avatar menu
283+
- Modify admin interface for creating site alerts to also create notifications
284+
- Add filter component to notifications page/table
285+
286+
Note: additional tickets will be needed to *register* the new notifications/emails on a per/notification basis

0 commit comments

Comments
 (0)