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

+18
View File
@@ -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