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

+48 -11
View File
@@ -1365,6 +1365,16 @@ func (s *Service) CancelBooking(ctx context.Context, principal domain.Principal,
return err
}
_ = s.repo.CancelPendingReminderJobsForBooking(ctx, booking.ID)
_ = s.repo.AppendActivityEvent(ctx, db.ActivityEventParams{
TenantID: membership.Tenant.ID,
ActorKind: "staff",
ActorSubject: principal.Subject,
ActorName: actorDisplayName(principal),
Action: "booking.cancelled",
EntityType: "booking",
EntityID: &booking.ID,
Summary: booking.CustomerName + " — " + s.bookingLabel(ctx, booking),
})
go s.sendBookingEmail(booking, false)
@@ -1395,10 +1405,46 @@ func (s *Service) PromoteBooking(ctx context.Context, principal domain.Principal
}
return err
}
_ = s.repo.AppendActivityEvent(ctx, db.ActivityEventParams{
TenantID: membership.Tenant.ID,
ActorKind: "staff",
ActorSubject: principal.Subject,
ActorName: actorDisplayName(principal),
Action: "booking.promoted",
EntityType: "booking",
EntityID: &promoted.ID,
Summary: promoted.CustomerName + " — " + s.bookingLabel(ctx, promoted),
})
go s.sendBookingEmail(promoted, true)
return nil
}
// bookingLabel resolves the display label for a booking — service name for
// appointments, session title for classes.
func (s *Service) bookingLabel(ctx context.Context, booking db.BookingRecord) string {
if booking.ServiceID != nil {
if service, err := s.repo.GetServiceByID(ctx, *booking.ServiceID); err == nil {
return service.Name
}
}
if booking.ClassSessionID != nil {
if session, err := s.repo.GetClassSessionByID(ctx, *booking.ClassSessionID); err == nil {
return session.Title
}
}
return ""
}
func actorDisplayName(principal domain.Principal) string {
if principal.Name != "" {
return principal.Name
}
if principal.Email != "" {
return principal.Email
}
return principal.Subject
}
// sendBookingEmail builds the shared payload and fires a confirmation
// (promoted=true) or cancellation notification. Failures are logged-and-done:
// email delivery must not fail the cancellation itself.
@@ -1419,17 +1465,8 @@ func (s *Service) sendBookingEmail(booking db.BookingRecord, promoted bool) {
brandColor = "#a65c3e"
}
var serviceName, locationName string
if booking.ServiceID != nil {
if service, err := s.repo.GetServiceByID(ctx, *booking.ServiceID); err == nil {
serviceName = service.Name
}
}
if booking.ClassSessionID != nil {
if session, err := s.repo.GetClassSessionByID(ctx, *booking.ClassSessionID); err == nil {
serviceName = session.Title
}
}
var locationName string
serviceName := s.bookingLabel(ctx, booking)
if booking.LocationID != nil {
if location, err := s.repo.GetLocationByID(ctx, *booking.LocationID); err == nil {
locationName = location.Name