feat(activity): per-tenant audit feed in notification dropdown

activity_events records booking lifecycle transitions with the actor
behind each change — staff (dashboard), customer (self-service), or
system (auto-promotion). The notification dropdown leads with the real
feed instead of derived recent-booking guesses.

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:06:13 +02:00
1 parent e5cb9bd543
commit d86cba61e3
18 files changed
+634 -28

No files matched your search

+55
View File
@@ -0,0 +1,55 @@
package activity
import (
"context"
"errors"
"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
}
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
}
events = append(events, event)
}
return domain.ActivityFeedResponse{Events: events}, nil
}