feat: notification enhancement

This commit is contained in:
Krispire Admin
2026-06-11 00:31:35 +05:30
parent cb62adf2de
commit 187bcd1442
11 changed files with 441 additions and 79 deletions
@@ -0,0 +1,99 @@
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
}