diff --git a/Rock/Model/Event/Attendance/Attendance.SaveHook.cs b/Rock/Model/Event/Attendance/Attendance.SaveHook.cs index b6fcae725b7..bf4ec876c2b 100644 --- a/Rock/Model/Event/Attendance/Attendance.SaveHook.cs +++ b/Rock/Model/Event/Attendance/Attendance.SaveHook.cs @@ -40,6 +40,8 @@ internal class SaveHook : EntitySaveHook private int? preSavePersonAliasId { get; set; } + private bool previousDidAttendValue { get; set; } + /// /// Method that will be called on an entity immediately before the item is saved by context /// @@ -49,7 +51,6 @@ protected override void PreSave() _isDeleted = State == EntityContextState.Deleted; - bool previousDidAttendValue; bool previouslyDeclined; if ( State == EntityContextState.Added ) @@ -67,11 +68,19 @@ protected override void PreSave() // if the record was changed to Declined, queue a GroupScheduleCancellationTransaction in PostSaveChanges _declinedScheduledAttendance = ( previouslyDeclined == false ) && Entity.IsScheduledPersonDeclined(); - if ( previousDidAttendValue == false && Entity.DidAttend == true ) - { - var launchMemberAttendedGroupWorkflowMsg = GetLaunchMemberAttendedGroupWorkflowMessage(); - launchMemberAttendedGroupWorkflowMsg.Send(); - } + /* + 06/21/2023 ETD + Launch the workflow in post save to avoid a race condition between the bus message and the saving of the Attendance record. + The LaunchMemberAttendedGroupWorkflow needs to be run post save to work correctly. + + if ( previousDidAttendValue == false && Entity.DidAttend == true ) + { + var launchMemberAttendedGroupWorkflowMsg = GetLaunchMemberAttendedGroupWorkflowMessage(); + launchMemberAttendedGroupWorkflowMsg.Send(); + } + + */ + var attendance = this.Entity; @@ -116,8 +125,7 @@ protected override void PreSave() RockContext.ExecuteAfterCommit( () => { // Use the fast queue for this because it is real-time. - new SendAttendanceRealTimeNotificationsTransaction( Entity.Guid, State == EntityContextState.Deleted ) - .Enqueue( true ); + new SendAttendanceRealTimeNotificationsTransaction( Entity.Guid, State == EntityContextState.Deleted ).Enqueue( true ); } ); } @@ -152,6 +160,13 @@ protected override void PostSave() StreakTypeService.HandleAttendanceRecord( Entity.Id ); } + // Do this in post save to avoid a race condition between the bus message and the saving of the Attendance record. See engineering note in PreSave(). + if ( previousDidAttendValue == false && Entity.DidAttend == true ) + { + var launchMemberAttendedGroupWorkflowMsg = GetLaunchMemberAttendedGroupWorkflowMessage(); + launchMemberAttendedGroupWorkflowMsg.Send(); + } + var rockContext = ( RockContext ) this.RockContext; if ( PersonAttendanceHistoryChangeList?.Any() == true ) @@ -226,44 +241,45 @@ private bool ShouldSendRealTimeMessage() private LaunchMemberAttendedGroupWorkflow.Message GetLaunchMemberAttendedGroupWorkflowMessage() { var launchMemberAttendedGroupWorkflowMsg = new LaunchMemberAttendedGroupWorkflow.Message(); - if ( State != EntityContextState.Deleted ) + if ( State == EntityContextState.Deleted ) + { + return launchMemberAttendedGroupWorkflowMsg; + } + + // Get the attendance record + var attendance = Entity as Attendance; + + // If attendance record is not valid or the DidAttend is false + if ( attendance == null || ( attendance.DidAttend.GetValueOrDefault( false ) == false ) ) { - // Get the attendance record - var attendance = Entity as Attendance; + return launchMemberAttendedGroupWorkflowMsg; + } - // If attendance record is valid and the DidAttend is true (not null or false) - if ( attendance != null && ( attendance.DidAttend == true ) ) - { - // Save for all adds - bool valid = State == EntityContextState.Added; + // Save for all adds + bool valid = State == EntityContextState.Added; - // If not an add, check previous DidAttend value - if ( !valid ) - { - // Only use changes where DidAttend was previously not true - valid = ( bool? ) Entry.OriginalValues.GetReadOnlyValueOrDefault( "DidAttend", false ) != true; - } + // If not an add, check previous DidAttend value + if ( !valid ) + { + // Only use changes where DidAttend was previously not true + valid = ( bool? ) Entry.OriginalValues.GetReadOnlyValueOrDefault( "DidAttend", false ) != true; + } - if ( valid ) - { - var occ = attendance.Occurrence; - if ( occ == null ) - { - occ = new AttendanceOccurrenceService( new RockContext() ).Get( attendance.OccurrenceId ); - } + if ( valid ) + { + var occ = attendance.Occurrence ?? new AttendanceOccurrenceService( new RockContext() ).Get( attendance.OccurrenceId ); - if ( occ != null ) - { - // Save the values - launchMemberAttendedGroupWorkflowMsg.GroupId = occ.GroupId; - launchMemberAttendedGroupWorkflowMsg.AttendanceDateTime = occ.OccurrenceDate; - launchMemberAttendedGroupWorkflowMsg.PersonAliasId = attendance.PersonAliasId; - - if ( occ.Group != null ) - { - launchMemberAttendedGroupWorkflowMsg.GroupTypeId = occ.Group.GroupTypeId; - } - } + if ( occ != null ) + { + // Save the values + launchMemberAttendedGroupWorkflowMsg.GroupId = occ.GroupId; + launchMemberAttendedGroupWorkflowMsg.AttendanceDateTime = occ.OccurrenceDate; + launchMemberAttendedGroupWorkflowMsg.PersonAliasId = attendance.PersonAliasId; + launchMemberAttendedGroupWorkflowMsg.AttendanceId = attendance.Id; + + if ( occ.Group != null ) + { + launchMemberAttendedGroupWorkflowMsg.GroupTypeId = occ.Group.GroupTypeId; } } } diff --git a/Rock/Tasks/LaunchMemberAttendedGroupWorkflow.cs b/Rock/Tasks/LaunchMemberAttendedGroupWorkflow.cs index eeab57ff2c3..12a2c793695 100644 --- a/Rock/Tasks/LaunchMemberAttendedGroupWorkflow.cs +++ b/Rock/Tasks/LaunchMemberAttendedGroupWorkflow.cs @@ -107,25 +107,38 @@ public override void Execute( Message message ) // Check to see if trigger is only specific to first time visitors if ( qualifierParts.Length > 4 && qualifierParts[4].AsBoolean() ) { - // Get the person from person alias + // Get the person from person alias, must match person because alias used in attendance record might be different int personId = new PersonAliasService( rockContext ) - .Queryable().AsNoTracking() + .Queryable() + .AsNoTracking() .Where( a => a.Id == message.PersonAliasId.Value ) .Select( a => a.PersonId ) .FirstOrDefault(); - // Check if there are any other attendances for this group/person and if so, do not launch workflow - if ( new AttendanceService( rockContext ) - .Queryable().AsNoTracking() - .Count( a => a.Occurrence.GroupId.HasValue && - a.Occurrence.GroupId.Value == message.GroupId.Value && - a.PersonAlias != null && - a.PersonAlias.PersonId == personId && - a.DidAttend.HasValue && - a.DidAttend.Value ) > 1 ) + // Get the attendance record, skip the trigger if one is not found (shouldn't happen) + var attendanceService = new AttendanceService( rockContext ); + var attendance = attendanceService.Get( message.AttendanceId.Value ); + if ( attendance == null ) { - launchIt = false; + continue; } + + // Count earlier attendances for the person/group, do not include this attendance. Match using either StartDateTime or CreatedDateTime in case the record was edited which would update the StartDateTime. + int count = attendanceService + .Queryable() + .AsNoTracking() + .Count( a => a.Id != message.AttendanceId + && ( a.StartDateTime < attendance.StartDateTime || a.CreatedDateTime < attendance.CreatedDateTime ) + && a.Occurrence.GroupId.HasValue + && a.Occurrence.GroupId.Value == message.GroupId.Value + && a.PersonAlias != null + && a.PersonAlias.PersonId == personId + && a.DidAttend.HasValue + && a.DidAttend.Value ); + + // Launch the workflow if this is the first attendance for the person/group + launchIt = count == 0; + } // If first time flag was not specified, or this is a first time visit, launch the workflow @@ -203,6 +216,14 @@ public sealed class Message : BusStartedTaskMessage /// Gets or sets the attendance date time. /// public DateTime? AttendanceDateTime { get; set; } + + /// + /// Gets or sets the attendance identifier. + /// + /// + /// The attendance identifier. + /// + public int? AttendanceId { get; set; } } } } \ No newline at end of file