package activity import ( "context" "errors" "time" "bookra/apps/backend/internal/db" "bookra/apps/backend/internal/domain" ) var ErrTenantMembership = errors.New("tenant membership not found") const defaultLimit = 50 const maxLimit = 100 type Service struct { repo db.Repository } func NewService(repo db.Repository) *Service { return &Service{repo: repo} } // List returns the tenant's recent activity, newest first. The feed is // tenant-scoped — every dashboard user sees the same audit trail. func (s *Service) List(ctx context.Context, principal domain.Principal, limit int) (domain.ActivityFeedResponse, error) { membership, err := s.repo.GetTenantMembershipBySubject(ctx, principal.Subject) if err != nil { return domain.ActivityFeedResponse{}, ErrTenantMembership } if limit <= 0 || limit > maxLimit { limit = defaultLimit } records, err := s.repo.ListActivityEvents(ctx, membership.Tenant.ID, limit) if err != nil { return domain.ActivityFeedResponse{}, err } readAt, err := s.repo.GetNotificationCursor(ctx, membership.Tenant.ID, principal.Subject) if err != nil { return domain.ActivityFeedResponse{}, err } unread := 0 events := make([]domain.ActivityEvent, 0, len(records)) for _, rec := range records { event := domain.ActivityEvent{ ID: rec.ID, ActorKind: rec.ActorKind, ActorName: rec.ActorName, Action: rec.Action, EntityType: rec.EntityType, Summary: rec.Summary, CreatedAt: rec.CreatedAt, } if rec.EntityID != nil { event.EntityID = *rec.EntityID } if readAt == nil || rec.CreatedAt.After(*readAt) { unread++ } events = append(events, event) } return domain.ActivityFeedResponse{Events: events, UnreadCount: unread, ReadAt: readAt}, nil } // MarkAllRead persists the user's cursor — events at or before now count as // read for this subject on every subsequent load. func (s *Service) MarkAllRead(ctx context.Context, principal domain.Principal) (time.Time, error) { membership, err := s.repo.GetTenantMembershipBySubject(ctx, principal.Subject) if err != nil { return time.Time{}, ErrTenantMembership } readAt := time.Now().UTC() if err := s.repo.SetNotificationCursor(ctx, membership.Tenant.ID, principal.Subject, readAt); err != nil { return time.Time{}, err } return readAt, nil }