100 lines
3.6 KiB
Go
100 lines
3.6 KiB
Go
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
|
||
}
|