feat: add eve chanting notif
This commit is contained in:
@@ -28,6 +28,11 @@ type ReminderCandidate struct {
|
||||
LastChantDate *time.Time // most recent completed chant day (UTC), nil if never
|
||||
ChantDoneToday bool
|
||||
QuoteDoneToday bool
|
||||
|
||||
// For the evening "incomplete chanting" reminder.
|
||||
RoundsToday int // rounds chanted so far today
|
||||
TargetRounds int // the user's daily target (16 if unset)
|
||||
LastChantIncompleteOn *time.Time // UTC date the evening reminder last fired
|
||||
}
|
||||
|
||||
// SetTimezone records the user's IANA timezone (validated by the caller).
|
||||
@@ -80,12 +85,18 @@ func (r *ReminderRepo) DueCandidates(ctx context.Context) ([]ReminderCandidate,
|
||||
AND l.log_date = (now() AT TIME ZONE 'UTC')::date),
|
||||
EXISTS(SELECT 1 FROM posts p
|
||||
WHERE p.user_id = u.id
|
||||
AND (p.created_at AT TIME ZONE 'UTC')::date = (now() AT TIME ZONE 'UTC')::date)
|
||||
AND (p.created_at AT TIME ZONE 'UTC')::date = (now() AT TIME ZONE 'UTC')::date),
|
||||
COALESCE((SELECT l.rounds_completed FROM sadhna_logs l
|
||||
WHERE l.user_id = u.id
|
||||
AND l.log_date = (now() AT TIME ZONE 'UTC')::date), 0),
|
||||
COALESCE(u.daily_chanting_rounds, 16),
|
||||
u.last_chant_incomplete_reminder_on
|
||||
FROM users u
|
||||
WHERE u.sadhna_reminders_enabled = TRUE
|
||||
AND (
|
||||
u.last_chant_reminder_on IS NULL OR u.last_chant_reminder_on < (now() AT TIME ZONE 'UTC')::date
|
||||
OR u.last_quote_reminder_on IS NULL OR u.last_quote_reminder_on < (now() AT TIME ZONE 'UTC')::date
|
||||
OR u.last_chant_incomplete_reminder_on IS NULL OR u.last_chant_incomplete_reminder_on < (now() AT TIME ZONE 'UTC')::date
|
||||
)`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("due candidates: %w", err)
|
||||
@@ -99,6 +110,7 @@ func (r *ReminderRepo) DueCandidates(ctx context.Context) ([]ReminderCandidate,
|
||||
&c.UserID, &c.Email, &c.TZ, &c.ChantTime, &c.QuoteTime,
|
||||
&c.LastChantOn, &c.LastQuoteOn, &c.LastChantDate,
|
||||
&c.ChantDoneToday, &c.QuoteDoneToday,
|
||||
&c.RoundsToday, &c.TargetRounds, &c.LastChantIncompleteOn,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("scan candidate: %w", err)
|
||||
}
|
||||
@@ -120,3 +132,11 @@ func (r *ReminderRepo) MarkQuoteReminded(ctx context.Context, userID string, utc
|
||||
`UPDATE users SET last_quote_reminder_on = $1 WHERE id = $2`, utcDate, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// MarkChantIncompleteReminded stamps the UTC date the evening "finish your
|
||||
// rounds" reminder went out, so it fires at most once per day.
|
||||
func (r *ReminderRepo) MarkChantIncompleteReminded(ctx context.Context, userID string, utcDate time.Time) error {
|
||||
_, err := r.pg.Pool.Exec(ctx,
|
||||
`UPDATE users SET last_chant_incomplete_reminder_on = $1 WHERE id = $2`, utcDate, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -32,8 +32,9 @@ const (
|
||||
|
||||
// Reminder kinds are delivered live (email + push + a websocket toast)
|
||||
// but NOT persisted — they're ephemeral nudges, not history.
|
||||
NotifChantReminder = "chant_reminder"
|
||||
NotifQuoteReminder = "quote_reminder"
|
||||
NotifChantReminder = "chant_reminder"
|
||||
NotifQuoteReminder = "quote_reminder"
|
||||
NotifChantIncomplete = "chant_incomplete_reminder"
|
||||
)
|
||||
|
||||
type NotificationService struct {
|
||||
|
||||
@@ -15,6 +15,7 @@ package service
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -37,6 +38,10 @@ const (
|
||||
// the overlap.
|
||||
reminderWindowMin = 20
|
||||
|
||||
// The evening "finish your rounds" reminder fires at 21:00 local. Kept
|
||||
// before the 21:30 quote reminder so the two don't collide in one tick.
|
||||
chantIncompleteTime = "21:00"
|
||||
|
||||
chantMsgStart = "Chant with Prabhupāda — start building your sādhana streak."
|
||||
chantMsgLapsed = "Just one hour of sādhana can save you from this duḥkhālayam aśāśvatam. Come and build your streak again."
|
||||
chantMsgYesterday = "Your sādhana was wonderful yesterday! Let's continue your sādhana today as well."
|
||||
@@ -112,6 +117,21 @@ func (s *ReminderService) RunOnce(ctx context.Context, now time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
// Evening "incomplete chanting" reminder — the devotee started their
|
||||
// rounds today (RoundsToday > 0) but hasn't reached the target by
|
||||
// 21:00 local. Only nudges those who began, so non-starters get just
|
||||
// the morning reminder, not a second one. Independent of the morning
|
||||
// chant reminder (own time + own per-day dedupe).
|
||||
if c.RoundsToday > 0 && !c.ChantDoneToday && notRemindedToday(c.LastChantIncompleteOn, utcToday) {
|
||||
if t := parseHHMM(chantIncompleteTime); t >= 0 && withinWindow(localMin, t) {
|
||||
s.notif.SendReminder(ctx, c.UserID, NotifChantIncomplete,
|
||||
"Finish your japa 🙏", incompleteChantMessage(c.RoundsToday, c.TargetRounds), s.sadhnaURL)
|
||||
if err := s.repo.MarkChantIncompleteReminded(ctx, c.UserID, utcToday); err != nil {
|
||||
s.logger.Warn("reminders: mark chant-incomplete failed", zap.String("user", c.UserID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quote reminder.
|
||||
if !c.QuoteDoneToday && notRemindedToday(c.LastQuoteOn, utcToday) {
|
||||
if t := parseHHMM(c.QuoteTime); t >= 0 && withinWindow(localMin, t) {
|
||||
@@ -143,6 +163,17 @@ func chantMessage(lastChant *time.Time, utcToday time.Time) string {
|
||||
}
|
||||
}
|
||||
|
||||
// incompleteChantMessage nudges a devotee who began their rounds today to
|
||||
// finish them (only sent when rounds > 0).
|
||||
func incompleteChantMessage(rounds, target int) string {
|
||||
if target <= 0 {
|
||||
target = 16
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"You've chanted %d of %d rounds today — finish your japa before the day ends. 🙏",
|
||||
rounds, target)
|
||||
}
|
||||
|
||||
// notRemindedToday is true when no reminder of this kind has gone out on the
|
||||
// current UTC day.
|
||||
func notRemindedToday(last *time.Time, utcToday time.Time) bool {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS last_chant_incomplete_reminder_on;
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Evening "incomplete chanting" reminder.
|
||||
--
|
||||
-- last_chant_incomplete_reminder_on: the UTC date the 9 PM (local)
|
||||
-- "finish your rounds" reminder was last sent, so it goes out at most once
|
||||
-- per day. Distinct from the morning chant reminder so a devotee can get a
|
||||
-- gentle start-of-day nudge and, if their rounds are still unfinished, an
|
||||
-- end-of-day one.
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_chant_incomplete_reminder_on DATE;
|
||||
Reference in New Issue
Block a user