From 135171d14192c49c9ce4ffd93139cde37781f585 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Tue, 22 Dec 2020 22:23:40 +0000 Subject: [PATCH] --- server/AyaNova/biz/NotifyEventHelper.cs | 989 +++++++++++------------- server/AyaNova/biz/UserBiz.cs | 35 + 2 files changed, 468 insertions(+), 556 deletions(-) diff --git a/server/AyaNova/biz/NotifyEventHelper.cs b/server/AyaNova/biz/NotifyEventHelper.cs index 73346de6..096b7f82 100644 --- a/server/AyaNova/biz/NotifyEventHelper.cs +++ b/server/AyaNova/biz/NotifyEventHelper.cs @@ -18,549 +18,6 @@ namespace AyaNova.Biz { private static ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger("NotifyEventProcessor"); - //Add operations message - internal static async Task AddOpsProblemEvent(string message, Exception ex = null) - { - if (string.IsNullOrWhiteSpace(message) && ex == null) - return; - - //Log as a backup in case there is no one to notify and also for the record and support - if (ex != null) - { - //actually, if there is an exception it's already logged anyway so don't re-log it here, just makes dupes - // log.LogError(ex, $"Ops problem notification: \"{message}\""); - message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(ex)}"; - } - else - log.LogWarning($"Ops problem notification: \"{message}\""); - - await AddGeneralNotifyEvent(NotifyEventType.ServerOperationsProblem, message, "OPS"); - } - - - //This handles general notification events not requiring a decision or tied to an object that are basically just a immediate message to the user - //e.g. ops problems, GeneralNotification, NotifyHealthCheck etc - //optional user id to send directly to them - internal static async Task AddGeneralNotifyEvent(NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0) - { - log.LogDebug($"AddGeneralNotifyEvent processing: [type:{eventType}, userId:{userId}, message:{message}]"); -#if (DEBUG) - switch (eventType) - { - case NotifyEventType.BackupStatus: - case NotifyEventType.GeneralNotification: - case NotifyEventType.NotifyHealthCheck://created by job processor itself - case NotifyEventType.ServerOperationsProblem: - break; - default://this will likely be a development error, not a production error so no need to log etc - throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - Type of event {eventType} is unexpected and not supported")); - } - - if (eventType != NotifyEventType.GeneralNotification && userId != 0) - { - throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - event {eventType} was specified with user id {userId} which is unexpected and not supported")); - } -#endif - - try - { - using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext) - { - - //General notification goes to a specific user only - //no need to consult subscriptions - if (eventType == NotifyEventType.GeneralNotification) - { - if (userId == 0) - { - //this will likely be a development error, not a production error so no need to log etc - throw new System.ArgumentException("NotifyEventProcessor:AddGeneralNotifyEvent: GeneralNotification requires a user id but none was specified"); - } - - var UserName = await ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => z.Name).FirstOrDefaultAsync(); - - //if they don't have a regular inapp subscription create one now - NotifySubscription defaultsub = await EnsureDefaultInAppUserNotificationSubscriptionExists(userId, ct); - - if (string.IsNullOrWhiteSpace(name)) - { - name = UserName; - } - - NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = userId, Message = message, NotifySubscriptionId = defaultsub.Id, Name = name }; - await ct.NotifyEvent.AddAsync(n); - await ct.SaveChangesAsync(); - return; - } - - - //check subscriptions for event and send accordingly to each user - var subs = await ct.NotifySubscription.Where(z => z.EventType == eventType).ToListAsync(); - - //append exception message if not null - if (except != null) - message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(except)}"; - - foreach (var sub in subs) - { - //note flag ~SERVER~ means to client to substitute "Server" translation key text instead - NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = sub.UserId, Message = message, NotifySubscriptionId = sub.Id, Name = "~SERVER~" }; - await ct.NotifyEvent.AddAsync(n); - } - if (subs.Count > 0) - await ct.SaveChangesAsync(); - } - - } - catch (Exception ex) - { - log.LogError(ex, $"Error adding general notify event [type:{eventType}, userId:{userId}, message:{message}]"); - } - - }//eom - - - - - - //This is told about an event and then determines if there are any subscriptions related to that event and proceses them accordingly - //todo: this should take some kind of general event type like the AyaEvent types (i.e. which CRUD operation is in effect if relevant) - //and also a biz object before and after or just before if not a change and also a AyaType - - //then *this* code will go through and look for subscriptions related to that event - //this way the biz object code can be "dumb" about notifications in general and just let this code handle it as needed - //will iterate the subscriptions and see if any apply here - internal static async Task HandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel newObject, ICoreBizObjectModel originalObject = null) - { - if (ServerBootConfig.SEEDING) return; - log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{newObject.AyaType}, AyaEvent:{ayaEvent}]"); - //set to true if any changes are made to the context (NotifyEvent added) - bool SaveContext = false; - try - { - using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext) - { - //short circuit if there are no subscriptions which would be typical of a v8 migrate scenario or fresh import or just not using notification - if (!await ct.NotifySubscription.AnyAsync()) - return; - - //switch through AyaEvent then individual Ayatypes as required for event types - switch (ayaEvent) - { - case AyaEvent.Created: - #region Created processing - //------------------------------ - // AyaType Specific created related subscriptions - // Note: these are for specific things only in this block - // generally being created notifications are further down below - switch (newObject.AyaType) - { - - case AyaType.Review: - { - //set a deadman automatic internal notification if goes past due - var r = (Review)newObject; - - //if not completed yet and not overdue already (which could indicate an import or something) - if (r.CompletedDate == null && r.DueDate > DateTime.UtcNow) - { - var userNotifySub = await EnsureDefaultInAppUserNotificationSubscriptionExists(r.UserId, ct); - NotifySubscription supervisorNotifySub = null; - if (r.UserId != r.AssignedByUserId) - { - supervisorNotifySub = await EnsureDefaultInAppUserNotificationSubscriptionExists(r.AssignedByUserId, ct); - } - - { - NotifyEvent n = new NotifyEvent() - { - EventType = NotifyEventType.GeneralNotification, - UserId = r.UserId, - ObjectId = newObject.Id, - AyaType = AyaType.Review, - NotifySubscriptionId = userNotifySub.Id, - Name = "LT:ReviewOverDue - " + newObject.Name, - EventDate = r.DueDate - }; - await ct.NotifyEvent.AddAsync(n); - log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - } - - if (supervisorNotifySub != null) - { - NotifyEvent n = new NotifyEvent() - { - EventType = NotifyEventType.GeneralNotification, - UserId = r.AssignedByUserId, - ObjectId = newObject.Id, - AyaType = AyaType.Review, - NotifySubscriptionId = supervisorNotifySub.Id, - Name = "LT:ReviewOverDue - " + newObject.Name, - EventDate = r.DueDate - }; - await ct.NotifyEvent.AddAsync(n); - log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - } - } - - } - break; - //AyaTypes with their own special notification related events - case AyaType.WorkOrder: - { - //WorkorderStatusChange - { - throw new System.NotImplementedException("Awaiting workorder object completion"); - var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.WorkorderStatusChange).ToListAsync(); - foreach (var sub in subs) - { - if (TagsMatch(newObject.Tags, sub.Tags)) - { - - NotifyEvent n = new NotifyEvent() - { - EventType = NotifyEventType.ObjectAge, - UserId = sub.UserId, - ObjectId = newObject.Id, - NotifySubscriptionId = sub.Id, - //TODO: IdValue=((WorkOrder)newObject).WorkorderStatusId - Name = newObject.Name - }; - await ct.NotifyEvent.AddAsync(n); - log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - - //Note: in same event but for MODIFY below if need to delete old notifyevent here - // var deleteEventList = await ct.NotifyEvent.Where(z => z.ObjectId == ev.ObjectId && z.AyaType == ev.AyaType).ToListAsync(); - // ct.NotifyEvent.RemoveRange(deleteEventList); - - - SaveContext = true; - } - } - } - //ScheduledOnWorkorder https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 - //ScheduledOnWorkorderImminent https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 - //WorkorderFinishStatusOverdue - //WorkorderFinished [USER, CUSTOMER], if customer then also CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - //OutsideServiceOverdue - //OutsideServiceReceived https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 - //PartRequestReceived https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 - //CustomerServiceImminent ALSO CopyOfCustomerNotification - //WorkorderCreatedForCustomer (conditional ID sb customer id) ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - - //PartRequested https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 - //WorkorderTotalExceedsThreshold - //WorkorderStatusAge - } - break; - case AyaType.Quote: - //QuoteStatusChange [USER, CUSTOMER] / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - //QuoteStatusAge - break; - case AyaType.Contract: - //ContractExpiring [CUSTOMER/USER] / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - - break; - case AyaType.Reminder: - //ReminderImminent - break; - case AyaType.Unit: - //UnitWarrantyExpiry - break; - case AyaType.UnitMeterReading: - //UnitMeterReadingMultipleExceeded - break; - - - } - - //----------------------------------------------- - //ObjectAge - // - // { - // var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectAge && z.AyaType == newObject.AyaType).ToListAsync(); - // foreach (var sub in subs) - // { - // if (TagsMatch(newObject.Tags, sub.Tags)) - // { - // //Note: age is set by advance notice which is consulted by CoreJobNotify in it's run so the deliver date is not required here only the reference EventDate to check for deliver - // //ObjectAge is determined by subscription AgeValue in combo with the EventDate NotifyEvent parameter which together determines at what age from notifyevent.EventDate it's considered for the event to have officially occured - // //However delivery is determined by sub.advancenotice so all three values play a part - // // - // NotifyEvent n = new NotifyEvent() - // { - // EventType = NotifyEventType.ObjectAge, - // UserId = sub.UserId, - // AyaType = newObject.AyaType, - // ObjectId = newObject.Id, - // NotifySubscriptionId = sub.Id, - // Name = newObject.Name - // }; - // await ct.NotifyEvent.AddAsync(n); - // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - // SaveContext = true; - // } - // } - // } - //------------------------------------------ - //ObjectCreated - // - // [USER for any type, CUSTOMER for workorder only] / if customer then CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - // { - // await ProcessStandardObjectCreatedEvents(newObject, ct); - // } - #endregion created processing - break; - case AyaEvent.Modified: - #region Modified processing - //######## NOTE: be sure to remove any that are being replaced potentially - //------------------------------ - // AyaType Specific modified related subscriptions - // - switch (newObject.AyaType) - { - case AyaType.Review: - { - // //Remove prior - // await ClearPriorEventsForObject(ct, newObject.AyaType, newObject.Id, NotifyEventType.GeneralNotification);//assumes only general event for this object type is overdue here - - // //set a deadman automatic internal notification if goes past due - // var r = (Review)newObject; - - // //it not completed yet and not overdue already (which could indicate an import or something) - // if (r.CompletedDate == null && r.DueDate > DateTime.UtcNow) - // { - // var userNotifySub = await EnsureDefaultInAppUserNotificationSubscriptionExists(r.UserId, ct); - // NotifySubscription supervisorNotifySub = null; - // if (r.UserId != r.AssignedByUserId) - // { - // supervisorNotifySub = await EnsureDefaultInAppUserNotificationSubscriptionExists(r.AssignedByUserId, ct); - // } - - // { - // NotifyEvent n = new NotifyEvent() - // { - // EventType = NotifyEventType.GeneralNotification, - // UserId = r.UserId, - // ObjectId = newObject.Id, - // AyaType = AyaType.Review, - // NotifySubscriptionId = userNotifySub.Id, - // Name = "LT:ReviewOverDue - " + newObject.Name, - // EventDate = r.DueDate - // }; - // await ct.NotifyEvent.AddAsync(n); - // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - // } - - // if (supervisorNotifySub != null) - // { - // NotifyEvent n = new NotifyEvent() - // { - // EventType = NotifyEventType.GeneralNotification, - // UserId = r.AssignedByUserId, - // ObjectId = newObject.Id, - // AyaType = AyaType.Review, - // NotifySubscriptionId = supervisorNotifySub.Id, - // Name = "LT:ReviewOverDue - " + newObject.Name, - // EventDate = r.DueDate - // }; - // await ct.NotifyEvent.AddAsync(n); - // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - // } - // } - - } - break; - - //AyaTypes with their own special notification related events - case AyaType.WorkOrder: - { - //WorkorderStatusChange [USER, CUSTOMER BOTH] / CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - // - { - throw new System.NotImplementedException("Awaiting workorder object completion"); - // if (((WorkOrder)newObject).WorkorderStatusId != ((WorkOrder)dbObject).WorkorderStatusId) - // { - // var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.WorkorderStatusChange).ToListAsync(); - // foreach (var sub in subs) - // { - // if (TagsMatch(newObject.Tags, sub.Tags)) - // { - - // NotifyEvent n = new NotifyEvent() - // { - // EventType = NotifyEventType.ObjectAge, - // UserId = sub.UserId, - // ObjectId = newObject.Id, - // NotifySubscriptionId = sub.Id, - // //TODO: IdValue=((WorkOrder)newObject).WorkorderStatusId - // EventDate = DateTime.UtcNow, - //Name=newObject.Name - //, - // Name=newObject.Name - // }; - // await ct.NotifyEvent.AddAsync(n); - // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - - // //Note: in same event but for MODIFY below if need to delete old notifyevent here - // // var deleteEventList = await ct.NotifyEvent.Where(z => z.ObjectId == ev.ObjectId && z.AyaType == ev.AyaType).ToListAsync(); - // // ct.NotifyEvent.RemoveRange(deleteEventList); - - - // SaveContext = true; - // } - // } - // } - } - //ScheduledOnWorkorder (DELETE OLD, USER / DATE COULD CHANGE) - //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 - //ScheduledOnWorkorderImminent (DELTE OLD) - //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 - //WorkorderFinishStatusOverdue (DELETE OLD) - //WorkorderFinished [USER, CUSTOMER] ALSO CopyOfCustomerNotification applies here https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - //OutsideServiceOverdue (ALSO DELETE OLD) - //OutsideServiceReceived - //PartRequestReceived - //CustomerServiceImminent (ALSO DELETE OLD) / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - //PartRequested - //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 - //WorkorderTotalExceedsThreshold - //WorkorderStatusAge (ALSO DELETE OLD) - //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/1137 - //WorkorderFinishedFollowUp (uses AGE after finished status) [USER] - - } - break; - case AyaType.Quote: - //QuoteStatusChange [USER, CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - //QuoteStatusAge (DELETE OLD) - break; - case AyaType.Contract: - //ContractExpiring (DELETE OLD) - break; - case AyaType.Reminder: - //ReminderImminent (DELETE OLD) - break; - case AyaType.Unit: - //UnitWarrantyExpiry (DELETE OLD) - break; - case AyaType.UnitMeterReading: - //UnitMeterReadingMultipleExceeded - //case 1254 - // This is as a threshold multiple value, i.e. if it's passing a multiple of 50,000 then notify, or notify every 10,000 or something. - // In saving code it just checks to see if it has crossed the multiple threshold, i.e. calculate the old multiple, calculate the new multiple if over then send notification immediate. - break; - case AyaType.CustomerServiceRequest: - //CSRAccepted (DELETE OLD might have changed from one to the other) [CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - //CSRRejected (DELETE OLD might have changed from one to the other) [CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 - - break; - case AyaType.ServiceBank: - //ServiceBankDepleted https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/472 - break; - - case AyaType.User: - //This one's a little different, if user has had roles changed, then pre-existing subs may not be allowed anymore - //Remove any notification subscriptions user doesn't have rights to: - if (((User)originalObject).Roles != ((User)newObject).Roles) - { - var DeleteList = new List(); - var NewRoles = ((User)newObject).Roles; - //iterate subs and remove any user shouldn't have - var userSubs = await ct.NotifySubscription.Where(z => z.UserId == newObject.Id).ToListAsync(); - foreach (var sub in userSubs) - { - if (sub.AyaType != AyaType.NoType) - { - //check if user has rights to it or not still - //must have read rights to be valid - if (!AyaNova.Api.ControllerHelpers.Authorized.HasAnyRole(NewRoles, sub.AyaType)) - { - //no rights whatsoever, so delete it - DeleteList.Add(sub.Id); - } - } - } - if (DeleteList.Count > 0) - { - var NSB = NotifySubscriptionBiz.GetBiz(ct); - foreach (var l in DeleteList) - { - await NSB.DeleteAsync(l); - } - } - } - break; - - - } - - - //------------------------------------------ - //ObjectModified - // - { - var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectModified && z.AyaType == newObject.AyaType).ToListAsync(); - foreach (var sub in subs) - { - if (TagsMatch(newObject.Tags, sub.Tags)) - { - NotifyEvent n = new NotifyEvent() - { - EventType = NotifyEventType.ObjectModified, - UserId = sub.UserId, - AyaType = newObject.AyaType, - ObjectId = newObject.Id, - NotifySubscriptionId = sub.Id, - Name = newObject.Name - }; - await ct.NotifyEvent.AddAsync(n); - log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - SaveContext = true; - } - } - } - #endregion modified processing - break; - case AyaEvent.Deleted: - #region Deleted processing - - //------------------------------ - // Delete any NotifyEvent records for this exact object - await ProcessStandardObjectDeletedEvents(newObject, ct); - #endregion deleted processing - break; - - - default: -#if (DEBUG) - throw (new System.NotSupportedException($"NotifyEventProcessor:HandlePotentialNotificationEvent - AyaEvent {ayaEvent} was specified which is unexpected and not supported")); -#else - break; -#endif - } - if (SaveContext) - await ct.SaveChangesAsync(); - } - } - catch (Exception ex) - { - log.LogError(ex, $"Error processing event"); - } - finally - { - log.LogDebug($"Notify event processing completed"); - - } - - - }//eom - - - - - - /////////////////////////////////////////// // ENSURE USER HAS IN APP NOTIFICATION @@ -589,24 +46,23 @@ namespace AyaNova.Biz } - - -///////////////////////////////////////// + ///////////////////////////////////////// // PROCESS STANDARD EVENTS // // public static async Task ProcessStandardObjectEvents(AyaEvent ayaEvent, ICoreBizObjectModel newObject, AyContext ct) { - switch(ayaEvent){ + switch (ayaEvent) + { case AyaEvent.Created: - await ProcessStandardObjectCreatedEvents(newObject,ct); - break; + await ProcessStandardObjectCreatedEvents(newObject, ct); + break; case AyaEvent.Deleted: - await ProcessStandardObjectDeletedEvents(newObject,ct); - break; - case AyaEvent.Modified: - await ProcessStandardObjectModifiedEvents(newObject,ct); - break; + await ProcessStandardObjectDeletedEvents(newObject, ct); + break; + case AyaEvent.Modified: + await ProcessStandardObjectModifiedEvents(newObject, ct); + break; } } @@ -662,7 +118,7 @@ namespace AyaNova.Biz }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - await ct.SaveChangesAsync(); + await ct.SaveChangesAsync(); } } } @@ -671,6 +127,36 @@ namespace AyaNova.Biz } + ///////////////////////////////////////// + // PROCESS STANDARD CREATE NOTIFICATION + // + // + public static async Task ProcessStandardObjectModifiedEvents(ICoreBizObjectModel newObject, AyContext ct) + { + { + var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectModified && z.AyaType == newObject.AyaType).ToListAsync(); + foreach (var sub in subs) + { + if (TagsMatch(newObject.Tags, sub.Tags)) + { + NotifyEvent n = new NotifyEvent() + { + EventType = NotifyEventType.ObjectModified, + UserId = sub.UserId, + AyaType = newObject.AyaType, + ObjectId = newObject.Id, + NotifySubscriptionId = sub.Id, + Name = newObject.Name + }; + await ct.NotifyEvent.AddAsync(n); + log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); + } + } + } + + + } + ///////////////////////////////////////// // PROCESS STANDARD DELETE NOTIFICATION // @@ -749,6 +235,397 @@ namespace AyaNova.Biz } + ///////////////////////////////////////// + // CREATE OPS PROBLEM EVENT + // + // + internal static async Task AddOpsProblemEvent(string message, Exception ex = null) + { + if (string.IsNullOrWhiteSpace(message) && ex == null) + return; + + //Log as a backup in case there is no one to notify and also for the record and support + if (ex != null) + { + //actually, if there is an exception it's already logged anyway so don't re-log it here, just makes dupes + // log.LogError(ex, $"Ops problem notification: \"{message}\""); + message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(ex)}"; + } + else + log.LogWarning($"Ops problem notification: \"{message}\""); + + await AddGeneralNotifyEvent(NotifyEventType.ServerOperationsProblem, message, "OPS"); + } + + + ///////////////////////////////////////// + // CREATE GENERAL NOTIFY EVENT + // + // + + internal static async Task AddGeneralNotifyEvent(NotifyEventType eventType, string message, string name, Exception except = null, long userId = 0) + { + //This handles general notification events not requiring a decision or tied to an object that are basically just a immediate message to the user + //e.g. ops problems, GeneralNotification, NotifyHealthCheck etc + //optional user id to send directly to them + log.LogDebug($"AddGeneralNotifyEvent processing: [type:{eventType}, userId:{userId}, message:{message}]"); +#if (DEBUG) + switch (eventType) + { + case NotifyEventType.BackupStatus: + case NotifyEventType.GeneralNotification: + case NotifyEventType.NotifyHealthCheck://created by job processor itself + case NotifyEventType.ServerOperationsProblem: + break; + default://this will likely be a development error, not a production error so no need to log etc + throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - Type of event {eventType} is unexpected and not supported")); + } + + if (eventType != NotifyEventType.GeneralNotification && userId != 0) + { + throw (new System.NotSupportedException($"NotifyEventProcessor:AddGeneralNotifyEvent - event {eventType} was specified with user id {userId} which is unexpected and not supported")); + } +#endif + + try + { + using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext) + { + + //General notification goes to a specific user only + //no need to consult subscriptions + if (eventType == NotifyEventType.GeneralNotification) + { + if (userId == 0) + { + //this will likely be a development error, not a production error so no need to log etc + throw new System.ArgumentException("NotifyEventProcessor:AddGeneralNotifyEvent: GeneralNotification requires a user id but none was specified"); + } + + var UserName = await ct.User.AsNoTracking().Where(z => z.Id == userId).Select(z => z.Name).FirstOrDefaultAsync(); + + //if they don't have a regular inapp subscription create one now + NotifySubscription defaultsub = await EnsureDefaultInAppUserNotificationSubscriptionExists(userId, ct); + + if (string.IsNullOrWhiteSpace(name)) + { + name = UserName; + } + + NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = userId, Message = message, NotifySubscriptionId = defaultsub.Id, Name = name }; + await ct.NotifyEvent.AddAsync(n); + await ct.SaveChangesAsync(); + return; + } + + + //check subscriptions for event and send accordingly to each user + var subs = await ct.NotifySubscription.Where(z => z.EventType == eventType).ToListAsync(); + + //append exception message if not null + if (except != null) + message += $"\nException error: {ExceptionUtil.ExtractAllExceptionMessages(except)}"; + + foreach (var sub in subs) + { + //note flag ~SERVER~ means to client to substitute "Server" translation key text instead + NotifyEvent n = new NotifyEvent() { EventType = eventType, UserId = sub.UserId, Message = message, NotifySubscriptionId = sub.Id, Name = "~SERVER~" }; + await ct.NotifyEvent.AddAsync(n); + } + if (subs.Count > 0) + await ct.SaveChangesAsync(); + } + + } + catch (Exception ex) + { + log.LogError(ex, $"Error adding general notify event [type:{eventType}, userId:{userId}, message:{message}]"); + } + + }//eom + + + }//eoc -}//eons \ No newline at end of file +}//eons + +//###DEPRECATED: MOVED EVENT PROCESSING INTO BIZ OBJECTS BUT +// //SPLIT INTO STANDARD EVENT PROCESSING METHODS BELOW +// //AND ANYTHING TYPE SPECIFIC IS IN BIZ OBJECT ITSELF +// //KEEPING THIS AS IT HAS SOME IMPLEMENTATION NOTES IN IT +// //This is told about an event and then determines if there are any subscriptions related to that event and proceses them accordingly +// //todo: this should take some kind of general event type like the AyaEvent types (i.e. which CRUD operation is in effect if relevant) +// //and also a biz object before and after or just before if not a change and also a AyaType + +// //then *this* code will go through and look for subscriptions related to that event +// //this way the biz object code can be "dumb" about notifications in general and just let this code handle it as needed +// //will iterate the subscriptions and see if any apply here +// internal static async Task HandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel newObject, ICoreBizObjectModel originalObject = null) +// { +// if (ServerBootConfig.SEEDING) return; +// log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{newObject.AyaType}, AyaEvent:{ayaEvent}]"); +// //set to true if any changes are made to the context (NotifyEvent added) +// bool SaveContext = false; +// try +// { +// using (AyContext ct = AyaNova.Util.ServiceProviderProvider.DBContext) +// { +// //short circuit if there are no subscriptions which would be typical of a v8 migrate scenario or fresh import or just not using notification +// if (!await ct.NotifySubscription.AnyAsync()) +// return; + +// //switch through AyaEvent then individual Ayatypes as required for event types +// switch (ayaEvent) +// { +// case AyaEvent.Created: +// #region Created processing +// //------------------------------ +// // AyaType Specific created related subscriptions +// // Note: these are for specific things only in this block +// // generally being created notifications are further down below +// switch (newObject.AyaType) +// { + + +// //AyaTypes with their own special notification related events +// case AyaType.WorkOrder: +// { +// //WorkorderStatusChange +// { +// throw new System.NotImplementedException("Awaiting workorder object completion"); +// var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.WorkorderStatusChange).ToListAsync(); +// foreach (var sub in subs) +// { +// if (TagsMatch(newObject.Tags, sub.Tags)) +// { + +// NotifyEvent n = new NotifyEvent() +// { +// EventType = NotifyEventType.ObjectAge, +// UserId = sub.UserId, +// ObjectId = newObject.Id, +// NotifySubscriptionId = sub.Id, +// //TODO: IdValue=((WorkOrder)newObject).WorkorderStatusId +// Name = newObject.Name +// }; +// await ct.NotifyEvent.AddAsync(n); +// log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); + +// //Note: in same event but for MODIFY below if need to delete old notifyevent here +// // var deleteEventList = await ct.NotifyEvent.Where(z => z.ObjectId == ev.ObjectId && z.AyaType == ev.AyaType).ToListAsync(); +// // ct.NotifyEvent.RemoveRange(deleteEventList); + + +// SaveContext = true; +// } +// } +// } +// //ScheduledOnWorkorder https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 +// //ScheduledOnWorkorderImminent https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 +// //WorkorderFinishStatusOverdue +// //WorkorderFinished [USER, CUSTOMER], if customer then also CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 +// //OutsideServiceOverdue +// //OutsideServiceReceived https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 +// //PartRequestReceived https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 +// //CustomerServiceImminent ALSO CopyOfCustomerNotification +// //WorkorderCreatedForCustomer (conditional ID sb customer id) ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 + +// //PartRequested https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 +// //WorkorderTotalExceedsThreshold +// //WorkorderStatusAge +// } +// break; +// case AyaType.Quote: +// //QuoteStatusChange [USER, CUSTOMER] / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 +// //QuoteStatusAge +// break; +// case AyaType.Contract: +// //ContractExpiring [CUSTOMER/USER] / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 + +// break; +// case AyaType.Reminder: +// //ReminderImminent +// break; +// case AyaType.Unit: +// //UnitWarrantyExpiry +// break; +// case AyaType.UnitMeterReading: +// //UnitMeterReadingMultipleExceeded +// break; + + +// } + + +// //------------------------------------------ +// //ObjectCreated +// // +// // [USER for any type, CUSTOMER for workorder only] / if customer then CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 +// // { + +// // } +// #endregion created processing +// break; +// case AyaEvent.Modified: +// #region Modified processing +// //######## NOTE: be sure to remove any that are being replaced potentially +// //------------------------------ +// // AyaType Specific modified related subscriptions +// // +// switch (newObject.AyaType) +// { + + +// //AyaTypes with their own special notification related events +// case AyaType.WorkOrder: +// { +// //WorkorderStatusChange [USER, CUSTOMER BOTH] / CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 +// // +// { +// throw new System.NotImplementedException("Awaiting workorder object completion"); +// // if (((WorkOrder)newObject).WorkorderStatusId != ((WorkOrder)dbObject).WorkorderStatusId) +// // { +// // var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.WorkorderStatusChange).ToListAsync(); +// // foreach (var sub in subs) +// // { +// // if (TagsMatch(newObject.Tags, sub.Tags)) +// // { + +// // NotifyEvent n = new NotifyEvent() +// // { +// // EventType = NotifyEventType.ObjectAge, +// // UserId = sub.UserId, +// // ObjectId = newObject.Id, +// // NotifySubscriptionId = sub.Id, +// // //TODO: IdValue=((WorkOrder)newObject).WorkorderStatusId +// // EventDate = DateTime.UtcNow, +// //Name=newObject.Name +// //, +// // Name=newObject.Name +// // }; +// // await ct.NotifyEvent.AddAsync(n); +// // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); + +// // //Note: in same event but for MODIFY below if need to delete old notifyevent here +// // // var deleteEventList = await ct.NotifyEvent.Where(z => z.ObjectId == ev.ObjectId && z.AyaType == ev.AyaType).ToListAsync(); +// // // ct.NotifyEvent.RemoveRange(deleteEventList); + + +// // SaveContext = true; +// // } +// // } +// // } +// } +// //ScheduledOnWorkorder (DELETE OLD, USER / DATE COULD CHANGE) +// //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 +// //ScheduledOnWorkorderImminent (DELTE OLD) +// //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 +// //WorkorderFinishStatusOverdue (DELETE OLD) +// //WorkorderFinished [USER, CUSTOMER] ALSO CopyOfCustomerNotification applies here https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 +// //OutsideServiceOverdue (ALSO DELETE OLD) +// //OutsideServiceReceived +// //PartRequestReceived +// //CustomerServiceImminent (ALSO DELETE OLD) / ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 +// //PartRequested +// //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/892 +// //WorkorderTotalExceedsThreshold +// //WorkorderStatusAge (ALSO DELETE OLD) +// //https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/1137 +// //WorkorderFinishedFollowUp (uses AGE after finished status) [USER] + +// } +// break; +// case AyaType.Quote: +// //QuoteStatusChange [USER, CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 +// //QuoteStatusAge (DELETE OLD) +// break; +// case AyaType.Contract: +// //ContractExpiring (DELETE OLD) +// break; +// case AyaType.Reminder: +// //ReminderImminent (DELETE OLD) +// break; +// case AyaType.Unit: +// //UnitWarrantyExpiry (DELETE OLD) +// break; +// case AyaType.UnitMeterReading: +// //UnitMeterReadingMultipleExceeded +// //case 1254 +// // This is as a threshold multiple value, i.e. if it's passing a multiple of 50,000 then notify, or notify every 10,000 or something. +// // In saving code it just checks to see if it has crossed the multiple threshold, i.e. calculate the old multiple, calculate the new multiple if over then send notification immediate. +// break; +// case AyaType.CustomerServiceRequest: +// //CSRAccepted (DELETE OLD might have changed from one to the other) [CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 +// //CSRRejected (DELETE OLD might have changed from one to the other) [CUSTOMER] ALSO CopyOfCustomerNotification https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/3398 + +// break; +// case AyaType.ServiceBank: +// //ServiceBankDepleted https://rockfish.ayanova.com/default.htm#!/rfcaseEdit/472 +// break; + + + + +// } + + +// //------------------------------------------ +// //ObjectModified +// // +// // { +// // var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ObjectModified && z.AyaType == newObject.AyaType).ToListAsync(); +// // foreach (var sub in subs) +// // { +// // if (TagsMatch(newObject.Tags, sub.Tags)) +// // { +// // NotifyEvent n = new NotifyEvent() +// // { +// // EventType = NotifyEventType.ObjectModified, +// // UserId = sub.UserId, +// // AyaType = newObject.AyaType, +// // ObjectId = newObject.Id, +// // NotifySubscriptionId = sub.Id, +// // Name = newObject.Name +// // }; +// // await ct.NotifyEvent.AddAsync(n); +// // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); +// // SaveContext = true; +// // } +// // } +// // } +// #endregion modified processing +// break; +// case AyaEvent.Deleted: +// #region Deleted processing + +// //------------------------------ +// // Delete any NotifyEvent records for this exact object +// await ProcessStandardObjectDeletedEvents(newObject, ct); +// #endregion deleted processing +// break; + + +// default: +// #if (DEBUG) +// throw (new System.NotSupportedException($"NotifyEventProcessor:HandlePotentialNotificationEvent - AyaEvent {ayaEvent} was specified which is unexpected and not supported")); +// #else +// break; +// #endif +// } +// if (SaveContext) +// await ct.SaveChangesAsync(); +// } +// } +// catch (Exception ex) +// { +// log.LogError(ex, $"Error processing event"); +// } +// finally +// { +// log.LogDebug($"Notify event processing completed"); + +// } + + +// }//eom \ No newline at end of file diff --git a/server/AyaNova/biz/UserBiz.cs b/server/AyaNova/biz/UserBiz.cs index 08454d0e..59b3d8e9 100644 --- a/server/AyaNova/biz/UserBiz.cs +++ b/server/AyaNova/biz/UserBiz.cs @@ -898,6 +898,41 @@ namespace AyaNova.Biz + + + case AyaType.User: + //USER MODIFIED SPECIAL + //This one's a little different, if user has had roles changed, then pre-existing subs may not be allowed anymore + //Remove any notification subscriptions user doesn't have rights to: + if (((User)originalObject).Roles != ((User)newObject).Roles) + { + var DeleteList = new List(); + var NewRoles = ((User)newObject).Roles; + //iterate subs and remove any user shouldn't have + var userSubs = await ct.NotifySubscription.Where(z => z.UserId == newObject.Id).ToListAsync(); + foreach (var sub in userSubs) + { + if (sub.AyaType != AyaType.NoType) + { + //check if user has rights to it or not still + //must have read rights to be valid + if (!AyaNova.Api.ControllerHelpers.Authorized.HasAnyRole(NewRoles, sub.AyaType)) + { + //no rights whatsoever, so delete it + DeleteList.Add(sub.Id); + } + } + } + if (DeleteList.Count > 0) + { + var NSB = NotifySubscriptionBiz.GetBiz(ct); + foreach (var l in DeleteList) + { + await NSB.DeleteAsync(l); + } + } + } + break; ///////////////////////////////////////////////////////////////////// }//eoc