2024-06-30 21:41:41 -04:00
package service
import (
"context"
"encoding/json"
"fmt"
"time"
chModel "donetick.com/core/internal/chore/model"
cModel "donetick.com/core/internal/circle/model"
cRepo "donetick.com/core/internal/circle/repo"
nModel "donetick.com/core/internal/notifier/model"
nRepo "donetick.com/core/internal/notifier/repo"
"donetick.com/core/logging"
)
type NotificationPlanner struct {
nRepo * nRepo . NotificationRepository
cRepo * cRepo . CircleRepository
}
func NewNotificationPlanner ( nr * nRepo . NotificationRepository , cr * cRepo . CircleRepository ) * NotificationPlanner {
return & NotificationPlanner { nRepo : nr ,
cRepo : cr ,
}
}
func ( n * NotificationPlanner ) GenerateNotifications ( c context . Context , chore * chModel . Chore ) bool {
log := logging . FromContext ( c )
circleMembers , err := n . cRepo . GetCircleUsers ( c , chore . CircleID )
2025-02-12 00:02:49 -05:00
var assignedUser * cModel . UserCircleDetail
2024-06-30 21:41:41 -04:00
for _ , member := range circleMembers {
2025-01-30 18:40:53 -05:00
if member . UserID == chore . AssignedTo {
2025-02-12 00:02:49 -05:00
assignedUser = member
break
2024-06-30 21:41:41 -04:00
}
}
if err != nil {
log . Error ( "Error getting circle members" , err )
return false
}
n . nRepo . DeleteAllChoreNotifications ( chore . ID )
notifications := make ( [ ] * nModel . Notification , 0 )
if ! chore . Notification || chore . FrequencyType == "trigger" {
return true
}
var mt * chModel . NotificationMetadata
if err := json . Unmarshal ( [ ] byte ( * chore . NotificationMetadata ) , & mt ) ; err != nil {
log . Error ( "Error unmarshalling notification metadata" , err )
2024-07-02 01:40:09 -04:00
return false
}
if chore . NextDueDate == nil {
2024-06-30 21:41:41 -04:00
return true
}
if mt . DueDate {
2025-02-12 00:02:49 -05:00
notifications = append ( notifications , generateDueNotifications ( chore , assignedUser ) )
2024-06-30 21:41:41 -04:00
}
if mt . PreDue {
2025-02-12 00:02:49 -05:00
notifications = append ( notifications , generatePreDueNotifications ( chore , assignedUser ) )
2024-06-30 21:41:41 -04:00
}
if mt . Nagging {
2025-02-12 00:02:49 -05:00
notifications = append ( notifications , generateOverdueNotifications ( chore , assignedUser ) ... )
2024-06-30 21:41:41 -04:00
}
2024-07-17 01:09:47 -04:00
if mt . CircleGroup {
notifications = append ( notifications , generateCircleGroupNotifications ( chore , mt ) ... )
}
2025-02-09 20:15:28 -05:00
log . Debug ( "Generated notifications" , "count" , len ( notifications ) )
2024-06-30 21:41:41 -04:00
n . nRepo . BatchInsertNotifications ( notifications )
return true
}
2025-02-12 00:02:49 -05:00
func generateDueNotifications ( chore * chModel . Chore , assignedUser * cModel . UserCircleDetail ) * nModel . Notification {
2025-02-11 22:17:42 -05:00
2025-02-12 00:02:49 -05:00
notification := & nModel . Notification {
ChoreID : chore . ID ,
IsSent : false ,
ScheduledFor : * chore . NextDueDate ,
CreatedAt : time . Now ( ) . UTC ( ) ,
TypeID : assignedUser . NotificationType ,
2025-02-11 22:17:42 -05:00
2025-02-12 00:02:49 -05:00
UserID : assignedUser . UserID ,
TargetID : assignedUser . TargetID ,
Text : fmt . Sprintf ( "📅 Reminder: *%s* is due today and assigned to %s." , chore . Name , assignedUser . DisplayName ) ,
RawEvent : map [ string ] interface { } {
"id" : chore . ID ,
"name" : chore . Name ,
"due_date" : chore . NextDueDate ,
"assignee" : assignedUser . DisplayName ,
"assignee_username" : assignedUser . Username ,
} ,
2024-06-30 21:41:41 -04:00
}
2025-02-12 00:02:49 -05:00
return notification
2024-06-30 21:41:41 -04:00
}
2025-02-12 00:02:49 -05:00
func generatePreDueNotifications ( chore * chModel . Chore , assignedUser * cModel . UserCircleDetail ) * nModel . Notification {
2025-02-11 22:17:42 -05:00
2025-02-12 00:02:49 -05:00
notification := & nModel . Notification {
ChoreID : chore . ID ,
IsSent : false ,
ScheduledFor : * chore . NextDueDate ,
CreatedAt : time . Now ( ) . UTC ( ) . Add ( - time . Hour * 3 ) ,
TypeID : assignedUser . NotificationType ,
UserID : assignedUser . UserID ,
CircleID : assignedUser . CircleID ,
TargetID : assignedUser . TargetID ,
2025-02-11 22:17:42 -05:00
2025-02-12 00:02:49 -05:00
Text : fmt . Sprintf ( "📢 Heads up! *%s* is due soon (on %s) and assigned to %s." , chore . Name , chore . NextDueDate . Format ( "January 2nd" ) , assignedUser . DisplayName ) ,
2024-06-30 21:41:41 -04:00
2025-02-12 00:02:49 -05:00
RawEvent : map [ string ] interface { } {
"id" : chore . ID ,
"name" : chore . Name ,
"due_date" : chore . NextDueDate ,
"assignee" : assignedUser . DisplayName ,
"assignee_username" : assignedUser . Username ,
} ,
2024-06-30 21:41:41 -04:00
}
2025-02-12 00:02:49 -05:00
return notification
2024-06-30 21:41:41 -04:00
}
2025-02-12 00:02:49 -05:00
func generateOverdueNotifications ( chore * chModel . Chore , assignedUser * cModel . UserCircleDetail ) [ ] * nModel . Notification {
var notifications [ ] * nModel . Notification
2024-06-30 21:41:41 -04:00
for _ , hours := range [ ] int { 24 , 48 , 72 } {
scheduleTime := chore . NextDueDate . Add ( time . Hour * time . Duration ( hours ) )
2025-02-12 00:02:49 -05:00
notification := & nModel . Notification {
ChoreID : chore . ID ,
IsSent : false ,
ScheduledFor : scheduleTime ,
CreatedAt : time . Now ( ) . UTC ( ) ,
TypeID : assignedUser . NotificationType ,
UserID : assignedUser . UserID ,
CircleID : assignedUser . CircleID ,
TargetID : fmt . Sprint ( assignedUser . TargetID ) ,
Text : fmt . Sprintf ( "🚨 *%s* is now %d hours overdue. Please complete it as soon as possible. (Assigned to %s)" , chore . Name , hours , assignedUser . DisplayName ) ,
RawEvent : map [ string ] interface { } {
"id" : chore . ID ,
"type" : EventTypeOverdue ,
"name" : chore . Name ,
"due_date" : chore . NextDueDate ,
"assignee" : assignedUser . DisplayName ,
"assignee_username" : assignedUser . Username ,
} ,
2024-06-30 21:41:41 -04:00
}
2025-02-12 00:02:49 -05:00
notifications = append ( notifications , notification )
2024-06-30 21:41:41 -04:00
}
return notifications
}
2024-07-17 01:09:47 -04:00
func generateCircleGroupNotifications ( chore * chModel . Chore , mt * chModel . NotificationMetadata ) [ ] * nModel . Notification {
var notifications [ ] * nModel . Notification
if ! mt . CircleGroup || mt . CircleGroupID == nil || * mt . CircleGroupID == 0 {
return notifications
}
if mt . DueDate {
2024-12-14 02:15:51 -05:00
notification := & nModel . Notification {
2024-07-17 01:09:47 -04:00
ChoreID : chore . ID ,
IsSent : false ,
ScheduledFor : * chore . NextDueDate ,
CreatedAt : time . Now ( ) . UTC ( ) ,
TypeID : 1 ,
TargetID : fmt . Sprint ( * mt . CircleGroupID ) ,
Text : fmt . Sprintf ( "📅 Reminder: *%s* is due today." , chore . Name ) ,
2025-02-09 20:15:28 -05:00
RawEvent : map [ string ] interface { } {
"id" : chore . ID ,
"type" : EventTypeDue ,
"name" : chore . Name ,
"due_date" : chore . NextDueDate . Format ( "January 2nd" ) ,
} ,
2024-12-14 02:15:51 -05:00
}
if notification . IsValid ( ) {
notifications = append ( notifications , notification )
}
2024-07-17 01:09:47 -04:00
}
if mt . PreDue {
2024-12-14 02:15:51 -05:00
notification := & nModel . Notification {
2024-07-17 01:09:47 -04:00
ChoreID : chore . ID ,
IsSent : false ,
ScheduledFor : * chore . NextDueDate ,
CreatedAt : time . Now ( ) . UTC ( ) . Add ( - time . Hour * 3 ) ,
TypeID : 3 ,
TargetID : fmt . Sprint ( * mt . CircleGroupID ) ,
Text : fmt . Sprintf ( "📢 Heads up! *%s* is due soon (on %s)." , chore . Name , chore . NextDueDate . Format ( "January 2nd" ) ) ,
2025-02-09 20:15:28 -05:00
RawEvent : map [ string ] interface { } {
"id" : chore . ID ,
"type" : EventTypePreDue ,
"name" : chore . Name ,
"due_date" : chore . NextDueDate . Format ( "January 2nd" ) ,
} ,
2024-12-14 02:15:51 -05:00
}
if notification . IsValid ( ) {
notifications = append ( notifications , notification )
}
2024-07-17 01:09:47 -04:00
}
if mt . Nagging {
for _ , hours := range [ ] int { 24 , 48 , 72 } {
scheduleTime := chore . NextDueDate . Add ( time . Hour * time . Duration ( hours ) )
2024-12-14 02:15:51 -05:00
notification := & nModel . Notification {
2024-07-17 01:09:47 -04:00
ChoreID : chore . ID ,
IsSent : false ,
ScheduledFor : scheduleTime ,
CreatedAt : time . Now ( ) . UTC ( ) ,
TypeID : 2 ,
TargetID : fmt . Sprint ( * mt . CircleGroupID ) ,
Text : fmt . Sprintf ( "🚨 *%s* is now %d hours overdue. Please complete it as soon as possible." , chore . Name , hours ) ,
2025-02-09 20:15:28 -05:00
RawEvent : map [ string ] interface { } {
"id" : chore . ID ,
"type" : EventTypeOverdue ,
"name" : chore . Name ,
"due_date" : chore . NextDueDate . Format ( "January 2nd" ) ,
} ,
2024-12-14 02:15:51 -05:00
}
if notification . IsValid ( ) {
notifications = append ( notifications , notification )
}
2024-07-17 01:09:47 -04:00
}
}
return notifications
}
2025-02-09 20:15:28 -05:00
type EventType string
const (
EventTypeUnknown EventType = "unknown"
EventTypeDue EventType = "due"
EventTypePreDue EventType = "pre_due"
EventTypeOverdue EventType = "overdue"
)