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

+78
View File
@@ -142,6 +142,10 @@ type Repository interface {
ListStaffFeedTokens(ctx context.Context, tenantID string) ([]StaffFeedTokenRecord, error)
RotateTenantFeedToken(ctx context.Context, tenantID string) (string, error)
RotateStaffFeedToken(ctx context.Context, staffID string) (string, error)
// Activity feed
AppendActivityEvent(ctx context.Context, params ActivityEventParams) error
ListActivityEvents(ctx context.Context, tenantID string, limit int) ([]ActivityEventRecord, error)
}
type TenantRecord struct {
@@ -298,6 +302,30 @@ type StaffFeedTokenRecord struct {
FeedToken string
}
// ActivityEventRecord / ActivityEventParams back the per-tenant audit feed.
type ActivityEventRecord struct {
ID string
TenantID string
ActorKind string
ActorName string
Action string
EntityType string
EntityID *string
Summary string
CreatedAt time.Time
}
type ActivityEventParams struct {
TenantID string
ActorKind string
ActorSubject string
ActorName string
Action string
EntityType string
EntityID *string
Summary string
}
type CreateStaffParams struct {
TenantID string
LocationID *string
@@ -819,6 +847,7 @@ type MemoryRepository struct {
emailTemplates []EmailTemplateRecord
tenantFeedToken string
staffFeedTokens map[string]string
activityEvents []ActivityEventRecord
}
func NewMemoryRepository() *MemoryRepository {
@@ -945,6 +974,26 @@ func NewMemoryRepository() *MemoryRepository {
staffFeedTokens: map[string]string{
staffID: "demo-staff-feed-token",
},
activityEvents: []ActivityEventRecord{
{
ID: uuid.NewString(), TenantID: tenantID, ActorKind: "staff", ActorName: "Demo Stylist",
Action: "booking.cancelled", EntityType: "booking",
Summary: "Carol White — Consultation",
CreatedAt: now.Add(-50 * time.Hour),
},
{
ID: uuid.NewString(), TenantID: tenantID, ActorKind: "system",
Action: "booking.promoted", EntityType: "booking",
Summary: "Bob Smith — Small group mobility class",
CreatedAt: now.Add(-26 * time.Hour),
},
{
ID: uuid.NewString(), TenantID: tenantID, ActorKind: "customer", ActorName: "Alice Johnson",
Action: "booking.created", EntityType: "booking",
Summary: "Alice Johnson — Relaxation massage",
CreatedAt: now.Add(-3 * time.Hour),
},
},
}
}
@@ -1315,6 +1364,35 @@ func (r *MemoryRepository) RotateStaffFeedToken(_ context.Context, staffID strin
return "", pgx.ErrNoRows
}
func (r *MemoryRepository) AppendActivityEvent(_ context.Context, params ActivityEventParams) error {
if params.TenantID != r.tenant.ID {
return nil
}
r.activityEvents = append(r.activityEvents, ActivityEventRecord{
ID: uuid.NewString(),
TenantID: params.TenantID,
ActorKind: params.ActorKind,
ActorName: params.ActorName,
Action: params.Action,
EntityType: params.EntityType,
EntityID: params.EntityID,
Summary: params.Summary,
CreatedAt: time.Now().UTC(),
})
return nil
}
func (r *MemoryRepository) ListActivityEvents(_ context.Context, tenantID string, limit int) ([]ActivityEventRecord, error) {
if tenantID != r.tenant.ID {
return nil, nil
}
var events []ActivityEventRecord
for i := len(r.activityEvents) - 1; i >= 0 && len(events) < limit; i-- {
events = append(events, r.activityEvents[i])
}
return events, nil
}
func (r *MemoryRepository) ListAvailabilityRulesByTenant(_ context.Context, tenantID string) ([]AvailabilityRuleRecord, error) {
if tenantID != r.tenant.ID {
return nil, nil