123 lines
4.3 KiB
Go
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
|
|
}
|