feat(notifications): persistent read cursor + delivery history

notification_cursors table stores one read-cursor per user per tenant;
GET /v1/activity returns unreadCount + readAt, POST /v1/activity/read
persists the cursor. Reminder dispatches now append reminder.sent /
reminder.failed system events so delivery history reaches the feed.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-18 16:55:21 +02:00
1 parent 76e45d4178
commit 2464aa45fa
14 files changed
+332 -8

No files matched your search

+29
View File
@@ -2,6 +2,10 @@ package db
import (
"context"
"errors"
"time"
"github.com/jackc/pgx/v5"
)
func (r *PGRepository) AppendActivityEvent(ctx context.Context, params ActivityEventParams) error {
@@ -40,3 +44,28 @@ func (r *PGRepository) ListActivityEvents(ctx context.Context, tenantID string,
}
return events, rows.Err()
}
func (r *PGRepository) GetNotificationCursor(ctx context.Context, tenantID string, userSubject string) (*time.Time, error) {
var readAt time.Time
err := r.pool.QueryRow(ctx, `
SELECT read_at FROM notification_cursors
WHERE tenant_id = $1 AND user_subject = $2
`, tenantID, userSubject).Scan(&readAt)
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return &readAt, nil
}
func (r *PGRepository) SetNotificationCursor(ctx context.Context, tenantID string, userSubject string, readAt time.Time) error {
_, err := r.pool.Exec(ctx, `
INSERT INTO notification_cursors (tenant_id, user_subject, read_at)
VALUES ($1, $2, $3)
ON CONFLICT (tenant_id, user_subject)
DO UPDATE SET read_at = EXCLUDED.read_at, updated_at = now()
`, tenantID, userSubject, readAt)
return err
}