feat: send schedules based on review feedback#841
feat: send schedules based on review feedback#841giresse19 wants to merge 4 commits intoNdoleStudio:mainfrom
Conversation
Greptile SummaryThis PR introduces a Send Schedules feature that lets users define weekly availability windows (day-of-week + minute ranges) and attach one schedule to each phone. Outgoing message notifications respect both the configured send rate and the selected schedule, delaying delivery to the next open window when necessary. Key additions:
Issues found:
Confidence Score: 4/5Safe to merge after addressing the ownership validation gap and the end_minute/UI mismatch; no data loss or security breach in the primary path Three P2 findings remain: dead code (
Important Files Changed
Sequence DiagramsequenceDiagram
participant Client
participant PhoneNotificationService
participant SendScheduleRepository
participant PhoneNotificationRepository
Client->>PhoneNotificationService: Schedule(params)
PhoneNotificationService->>PhoneNotificationService: Load phone by ID
alt phone.ScheduleID != nil
PhoneNotificationService->>SendScheduleRepository: Load(userID, scheduleID)
SendScheduleRepository-->>PhoneNotificationService: schedule / 404
Note over PhoneNotificationService: If 404, schedule = nil
end
PhoneNotificationService->>PhoneNotificationRepository: Schedule(messagesPerMinute, schedule, notification)
alt messagesPerMinute == 0
PhoneNotificationRepository->>PhoneNotificationRepository: resolveScheduledAt(now, schedule)
PhoneNotificationRepository-->>PhoneNotificationService: insert notification
else messagesPerMinute > 0
PhoneNotificationRepository->>PhoneNotificationRepository: load last notification (tx lock)
PhoneNotificationRepository->>PhoneNotificationRepository: resolveScheduledAt(now, schedule)
PhoneNotificationRepository->>PhoneNotificationRepository: rateLimitedAt = lastScheduledAt + 60/rate
PhoneNotificationRepository->>PhoneNotificationRepository: resolveScheduledAt(max(now, rateLimitedAt), schedule)
PhoneNotificationRepository-->>PhoneNotificationService: insert notification with scheduled time
end
Reviews (2): Last reviewed commit: "feat: Refactor send schedules based on r..." | Re-trigger Greptile |
| return nil | ||
| } | ||
|
|
||
| func (repository *gormPhoneNotificationRepository) resolveScheduledAt(current time.Time, schedule *entities.SendSchedule) time.Time { | ||
| if schedule == nil || !schedule.IsActive || len(schedule.Windows) == 0 { | ||
| return current.UTC() | ||
| } | ||
|
|
||
| location, err := time.LoadLocation(schedule.Timezone) | ||
| if err != nil { | ||
| return current.UTC() | ||
| } | ||
|
|
||
| base := current.In(location) | ||
| var best time.Time | ||
| for dayOffset := 0; dayOffset <= 7; dayOffset++ { | ||
| day := base.AddDate(0, 0, dayOffset) | ||
| weekday := int(day.Weekday()) | ||
| for _, window := range schedule.Windows { | ||
| if window.DayOfWeek != weekday { | ||
| continue | ||
| } | ||
|
|
||
| start := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, location).Add(time.Duration(window.StartMinute) * time.Minute) | ||
| end := time.Date(day.Year(), day.Month(), day.Day(), 0, 0, 0, 0, location).Add(time.Duration(window.EndMinute) * time.Minute) | ||
|
|
||
| var candidate time.Time | ||
| switch { | ||
| case dayOffset == 0 && base.Before(start): | ||
| candidate = start | ||
| case dayOffset == 0 && (base.Equal(start) || (base.After(start) && base.Before(end))): | ||
| candidate = base | ||
| case dayOffset > 0: | ||
| candidate = start | ||
| default: | ||
| continue | ||
| } | ||
|
|
||
| if best.IsZero() || candidate.Before(best) { | ||
| best = candidate | ||
| } | ||
| } | ||
| if !best.IsZero() { | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if best.IsZero() { | ||
| return current.UTC() | ||
| } | ||
| return best.UTC() | ||
| } | ||
|
|
||
| func (repository *gormPhoneNotificationRepository) maxTime(a, b time.Time) time.Time { | ||
| if a.Unix() > b.Unix() { | ||
| return a |
There was a problem hiding this comment.
Duplicate window-resolution logic
resolveScheduledAt here is an exact copy of the algorithm already implemented as ResolveScheduledSendTime on SendScheduleService (introduced in send_schedule_service.go). Maintaining two independent copies of the same scheduling logic will inevitably lead to subtle divergences.
The service method returns (time.Time, error) and already handles the timezone-error path, so the notification service (or even the repository, if it held a reference to the service) could delegate to that single implementation instead of duplicating it here.
| input.Name = strings.TrimSpace(input.Name) | ||
| input.Timezone = strings.TrimSpace(input.Timezone) | ||
| windows := make([]SendScheduleWindow, 0, len(input.Windows)) | ||
| for _, item := range input.Windows { | ||
| windows = append(windows, SendScheduleWindow{DayOfWeek: item.DayOfWeek, StartMinute: item.StartMinute, EndMinute: item.EndMinute}) | ||
| } | ||
| sort.SliceStable(windows, func(i, j int) bool { | ||
| if windows[i].DayOfWeek == windows[j].DayOfWeek { | ||
| return windows[i].StartMinute < windows[j].StartMinute | ||
| } | ||
| return windows[i].DayOfWeek < windows[j].DayOfWeek | ||
| }) | ||
| input.Windows = windows | ||
| return *input | ||
| } | ||
|
|
||
| func (input *SendScheduleStore) ToParams(user entities.AuthContext) *services.SendScheduleUpsertParams { | ||
| windows := make([]entities.SendScheduleWindow, 0, len(input.Windows)) | ||
| for _, item := range input.Windows { |
There was a problem hiding this comment.
…le-review-fixes # Conflicts: # web/pages/settings/index.vue
No description provided.