package repository import ( "context" "fmt" ) // Notification delivery channels (notification_prefs.channel values). const ( ChannelInApp = "in_app" ChannelEmail = "email" ChannelPush = "push" ) // PrefKey is the lookup key for a (kind, channel) cell in a DisabledSet. func PrefKey(kind, channel string) string { return kind + "/" + channel } // NotificationPrefsRepo stores per-user notification opt-outs: one row per // (kind, channel) the user has turned OFF. Absence of a row means enabled, // so the table only ever holds overrides and new kinds/channels default on. type NotificationPrefsRepo struct{ pg *Postgres } func NewNotificationPrefsRepo(pg *Postgres) *NotificationPrefsRepo { return &NotificationPrefsRepo{pg: pg} } // DisabledSet returns the user's opt-outs keyed by PrefKey(kind, channel). func (r *NotificationPrefsRepo) DisabledSet(ctx context.Context, userID string) (map[string]struct{}, error) { rows, err := r.pg.Pool.Query(ctx, ` SELECT kind, channel FROM notification_prefs WHERE user_id = $1 AND enabled = FALSE`, userID) if err != nil { return nil, fmt.Errorf("list notification prefs: %w", err) } defer rows.Close() out := map[string]struct{}{} for rows.Next() { var kind, channel string if err := rows.Scan(&kind, &channel); err != nil { return nil, fmt.Errorf("scan notification pref: %w", err) } out[PrefKey(kind, channel)] = struct{}{} } return out, rows.Err() } // Disable records opt-out rows for every kind × channel pair. func (r *NotificationPrefsRepo) Disable(ctx context.Context, userID string, kinds, channels []string) error { _, err := r.pg.Pool.Exec(ctx, ` INSERT INTO notification_prefs (user_id, kind, channel, enabled) SELECT $1, k, c, FALSE FROM unnest($2::text[]) AS k, unnest($3::text[]) AS c ON CONFLICT (user_id, kind, channel) DO UPDATE SET enabled = FALSE, updated_at = now()`, userID, kinds, channels) if err != nil { return fmt.Errorf("disable notification prefs: %w", err) } return nil } // Enable removes opt-out rows (absence = enabled) for every kind × channel // pair. func (r *NotificationPrefsRepo) Enable(ctx context.Context, userID string, kinds, channels []string) error { _, err := r.pg.Pool.Exec(ctx, ` DELETE FROM notification_prefs WHERE user_id = $1 AND kind = ANY($2::text[]) AND channel = ANY($3::text[])`, userID, kinds, channels) if err != nil { return fmt.Errorf("enable notification prefs: %w", err) } return nil } // SyncLegacyColumns recomputes users.like_emails_enabled and // users.sadhna_reminders_enabled from the prefs table, so an old frontend // (or a rollback) reading the legacy columns stays correct. Call after any // prefs write; drop together with the columns once fully rolled out. func (r *NotificationPrefsRepo) SyncLegacyColumns(ctx context.Context, userID string) error { _, err := r.pg.Pool.Exec(ctx, ` UPDATE users SET like_emails_enabled = NOT EXISTS ( SELECT 1 FROM notification_prefs np WHERE np.user_id = users.id AND np.kind = 'post_like' AND np.channel = 'email' AND np.enabled = FALSE), -- The legacy reminder toggle was all-or-nothing: it reads as OFF -- only when every reminder kind × channel cell is opted out. sadhna_reminders_enabled = ( SELECT count(*) FROM notification_prefs np WHERE np.user_id = users.id AND np.kind IN ('chant_reminder', 'quote_reminder', 'chant_incomplete_reminder') AND np.enabled = FALSE) < 9 WHERE id = $1`, userID) if err != nil { return fmt.Errorf("sync legacy notification prefs: %w", err) } return nil }