Files
spsays-bff/internal/repository/reminder_repo.go
T

123 lines
4.3 KiB
Go

package repository
import (
"context"
"fmt"
"time"
)
// ReminderRepo backs the sādhana reminder scheduler: per-user preferences,
// the timezone used to fire at local time, and the candidate query the
// worker scans each tick.
type ReminderRepo struct{ pg *Postgres }
func NewReminderRepo(pg *Postgres) *ReminderRepo { return &ReminderRepo{pg: pg} }
// ReminderCandidate is one user the worker evaluates per tick. The time-of-day
// windowing + per-day dedupe decision happens in Go (using TZ); the SQL just
// supplies the inputs, including whether today's disciplines are already done
// (bucketed by UTC day, matching the streak logic).
type ReminderCandidate struct {
UserID string
Email *string
TZ string
ChantTime string // "HH:MM" local
QuoteTime string // "HH:MM" local
LastChantOn *time.Time
LastQuoteOn *time.Time
LastChantDate *time.Time // most recent completed chant day (UTC), nil if never
ChantDoneToday bool
QuoteDoneToday bool
}
// SetTimezone records the user's IANA timezone (validated by the caller).
func (r *ReminderRepo) SetTimezone(ctx context.Context, userID, tz string) error {
_, err := r.pg.Pool.Exec(ctx, `UPDATE users SET timezone = $1 WHERE id = $2`, tz, userID)
if err != nil {
return fmt.Errorf("set timezone: %w", err)
}
return nil
}
// GetEnabled returns whether the user wants sādhana reminders.
func (r *ReminderRepo) GetEnabled(ctx context.Context, userID string) (bool, error) {
var enabled bool
err := r.pg.Pool.QueryRow(ctx,
`SELECT sadhna_reminders_enabled FROM users WHERE id = $1`, userID).Scan(&enabled)
if err != nil {
return false, fmt.Errorf("get reminder pref: %w", err)
}
return enabled, nil
}
// SetEnabled flips the user's reminder opt-in.
func (r *ReminderRepo) SetEnabled(ctx context.Context, userID string, enabled bool) error {
_, err := r.pg.Pool.Exec(ctx,
`UPDATE users SET sadhna_reminders_enabled = $1 WHERE id = $2`, enabled, userID)
if err != nil {
return fmt.Errorf("set reminder pref: %w", err)
}
return nil
}
// DueCandidates returns reminder-enabled users who haven't already had BOTH
// reminders today, with the data the worker needs to decide. Defaults are
// folded in (06:00 chant, 21:30 quote, UTC tz).
func (r *ReminderRepo) DueCandidates(ctx context.Context) ([]ReminderCandidate, error) {
rows, err := r.pg.Pool.Query(ctx, `
SELECT
u.id,
u.email,
COALESCE(NULLIF(u.timezone, ''), 'UTC'),
COALESCE(NULLIF(u.preferred_chanting_time, ''), '06:00'),
COALESCE(NULLIF(u.peak_active_time, ''), '21:30'),
u.last_chant_reminder_on,
u.last_quote_reminder_on,
(SELECT max(l.log_date) FROM sadhna_logs l
WHERE l.user_id = u.id AND l.chant_done),
EXISTS(SELECT 1 FROM sadhna_logs l
WHERE l.user_id = u.id AND l.chant_done
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)
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
)`)
if err != nil {
return nil, fmt.Errorf("due candidates: %w", err)
}
defer rows.Close()
var out []ReminderCandidate
for rows.Next() {
var c ReminderCandidate
if err := rows.Scan(
&c.UserID, &c.Email, &c.TZ, &c.ChantTime, &c.QuoteTime,
&c.LastChantOn, &c.LastQuoteOn, &c.LastChantDate,
&c.ChantDoneToday, &c.QuoteDoneToday,
); err != nil {
return nil, fmt.Errorf("scan candidate: %w", err)
}
out = append(out, c)
}
return out, rows.Err()
}
// MarkChantReminded / MarkQuoteReminded stamp the UTC date a reminder went
// out, so the same kind won't fire again that day.
func (r *ReminderRepo) MarkChantReminded(ctx context.Context, userID string, utcDate time.Time) error {
_, err := r.pg.Pool.Exec(ctx,
`UPDATE users SET last_chant_reminder_on = $1 WHERE id = $2`, utcDate, userID)
return err
}
func (r *ReminderRepo) MarkQuoteReminded(ctx context.Context, userID string, utcDate time.Time) error {
_, err := r.pg.Pool.Exec(ctx,
`UPDATE users SET last_quote_reminder_on = $1 WHERE id = $2`, utcDate, userID)
return err
}