mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
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:
1 parent
76e45d4178
commit
2464aa45fa
14 files changed
+332
-8
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -146,6 +146,9 @@ type Repository interface {
|
||||
// Activity feed
|
||||
AppendActivityEvent(ctx context.Context, params ActivityEventParams) error
|
||||
ListActivityEvents(ctx context.Context, tenantID string, limit int) ([]ActivityEventRecord, error)
|
||||
// GetNotificationCursor returns nil when the user has no cursor yet.
|
||||
GetNotificationCursor(ctx context.Context, tenantID string, userSubject string) (*time.Time, error)
|
||||
SetNotificationCursor(ctx context.Context, tenantID string, userSubject string, readAt time.Time) error
|
||||
|
||||
// Staff metrics
|
||||
GetStaffMetrics(ctx context.Context, tenantID string, from time.Time, to time.Time) ([]StaffMetricsRecord, error)
|
||||
@@ -864,6 +867,7 @@ type MemoryRepository struct {
|
||||
tenantFeedToken string
|
||||
staffFeedTokens map[string]string
|
||||
activityEvents []ActivityEventRecord
|
||||
notificationCursors map[string]time.Time
|
||||
}
|
||||
|
||||
func NewMemoryRepository() *MemoryRepository {
|
||||
@@ -990,6 +994,7 @@ func NewMemoryRepository() *MemoryRepository {
|
||||
staffFeedTokens: map[string]string{
|
||||
staffID: "demo-staff-feed-token",
|
||||
},
|
||||
notificationCursors: map[string]time.Time{},
|
||||
activityEvents: []ActivityEventRecord{
|
||||
{
|
||||
ID: uuid.NewString(), TenantID: tenantID, ActorKind: "staff", ActorName: "Demo Stylist",
|
||||
@@ -1409,6 +1414,19 @@ func (r *MemoryRepository) ListActivityEvents(_ context.Context, tenantID string
|
||||
return events, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) GetNotificationCursor(_ context.Context, tenantID string, userSubject string) (*time.Time, error) {
|
||||
readAt, ok := r.notificationCursors[tenantID+"/"+userSubject]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return &readAt, nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) SetNotificationCursor(_ context.Context, tenantID string, userSubject string, readAt time.Time) error {
|
||||
r.notificationCursors[tenantID+"/"+userSubject] = readAt
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MemoryRepository) ListAvailabilityRulesByTenant(_ context.Context, tenantID string) ([]AvailabilityRuleRecord, error) {
|
||||
if tenantID != r.tenant.ID {
|
||||
return nil, nil
|
||||
|
||||
Reference in new issue
Block a user