From 58b05dd1f3241ffa7ff522001af97ec741ee9ec1 Mon Sep 17 00:00:00 2001 From: John Cardinal Date: Fri, 30 Jul 2021 21:11:09 +0000 Subject: [PATCH] --- server/AyaNova/biz/PMBiz.cs | 227 +----------------- server/AyaNova/biz/QuoteBiz.cs | 419 +++++++++++++-------------------- 2 files changed, 163 insertions(+), 483 deletions(-) diff --git a/server/AyaNova/biz/PMBiz.cs b/server/AyaNova/biz/PMBiz.cs index 9cb521e8..83376313 100644 --- a/server/AyaNova/biz/PMBiz.cs +++ b/server/AyaNova/biz/PMBiz.cs @@ -344,7 +344,7 @@ namespace AyaNova.Biz AddError(ApiErrorCode.NOT_FOUND); return false; } - PMValidateCanDelete(dbObject); + await PMValidateCanDelete(dbObject); if (HasErrors) return false; @@ -615,72 +615,6 @@ namespace AyaNova.Biz } - - - //////////////////////////////////////////////////////////////////////////////////////////////// - // "The Andy" notification helper - // - // (for now this is only for the notification exceeds total so only need one grand total of - // line totals, if in future need more can return a Record object instead with split out - // taxes, net etc etc) - // - private async Task WorkorderGrandTotalAsync(long workOrderId, AyContext ct) - { - var wo = await ct.PM.AsNoTracking().AsSplitQuery() - .Include(w => w.Items.OrderBy(item => item.Sequence)) - .ThenInclude(wi => wi.Expenses) - .Include(w => w.Items) - .ThenInclude(wi => wi.Labors) - .Include(w => w.Items) - .ThenInclude(wi => wi.Loans) - .Include(w => w.Items) - .ThenInclude(wi => wi.Parts) - .Include(w => w.Items) - .ThenInclude(wi => wi.Travels) - .Include(w => w.Items) - .ThenInclude(wi => wi.OutsideServices) - .SingleOrDefaultAsync(z => z.Id == workOrderId); - if (wo == null) return 0m; - - decimal GrandTotal = 0m; - //update pricing - foreach (PMItem wi in wo.Items) - { - foreach (PMItemExpense o in wi.Expenses) - await ExpensePopulateVizFields(o, true); - foreach (PMItemLabor o in wi.Labors) - await LaborPopulateVizFields(o, true); - foreach (PMItemLoan o in wi.Loans) - await LoanPopulateVizFields(o, null, true); - foreach (PMItemPart o in wi.Parts) - await PartPopulateVizFields(o, true); - foreach (PMItemTravel o in wi.Travels) - await TravelPopulateVizFields(o, true); - foreach (PMItemOutsideService o in wi.OutsideServices) - await OutsideServicePopulateVizFields(o, true); - } - - foreach (PMItem wi in wo.Items) - { - foreach (PMItemExpense o in wi.Expenses) - GrandTotal += o.LineTotalViz; - foreach (PMItemLabor o in wi.Labors) - GrandTotal += o.LineTotalViz; - foreach (PMItemLoan o in wi.Loans) - GrandTotal += o.LineTotalViz; - foreach (PMItemPart o in wi.Parts) - GrandTotal += o.LineTotalViz; - foreach (PMItemTravel o in wi.Travels) - GrandTotal += o.LineTotalViz; - foreach (PMItemOutsideService o in wi.OutsideServices) - GrandTotal += o.LineTotalViz; - } - - return GrandTotal; - } - - - //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // @@ -726,42 +660,6 @@ namespace AyaNova.Biz AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "RepeatInterval", await Translate("ErrorRepeatIntervalTooSmall")); } - - - - /* - - todo: quote status list first, it's a table of created items, keep properties from v7 but add the following properties: - SelectRoles - who can select the status (still shows if they can't select but that's the current status, like active does) - This is best handled at the client. It prefetches all the status out of the normal picklist process, more like how other things are separately handled now without a picklist - client then knows if a status is available or not and can process to only present available ones - #### Server can use a biz rule to ensure that it can't be circumvented - UI defaults to any role - DeselectRoles - who can unset this status (important for process control) - UI defaults to any role - CompletedStatus bool - this is a final status indicating all work on the quote is completed, affects notification etc - UI defaults to false but when set to true auto sets lockworkorder to true (but user can just unset lockworkorder) - LockWorkorder - this status is considered read only and the quote is locked - Just a read only thing, can just change status to "unlock" it - to support states where no one should work on a wo for whatever reason but it's not necessarily completed - e.g. "Hold for inspection", "On hold" generally etc - */ - // //Name required - // if (string.IsNullOrWhiteSpace(proposedObj.Name)) - // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); - - - // //If name is otherwise OK, check that name is unique - // if (!PropertyHasErrors("Name")) - // { - // //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false - // if (await ct.PM.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id)) - // { - // AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); - // } - // } - - //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.PM.ToString()); if (FormCustomization != null) @@ -774,12 +672,11 @@ namespace AyaNova.Biz //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } - } - private void PMValidateCanDelete(PM dbObject) + private async Task PMValidateCanDelete(PM dbObject) { //Check restricted role preventing create if (UserIsRestrictedType) @@ -788,41 +685,12 @@ namespace AyaNova.Biz return;//this is a completely disqualifying error } //FOREIGN KEY CHECKS - //these are examples copied from customer for when other objects are actually referencing them - // if (await ct.User.AnyAsync(m => m.CustomerId == inObj.Id)) - // AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User")); - // if (await ct.Unit.AnyAsync(m => m.CustomerId == inObj.Id)) - // AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Unit")); - // if (await ct.CustomerServiceRequest.AnyAsync(m => m.CustomerId == inObj.Id)) - // AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("CustomerServiceRequest")); - // if (await ct.PurchaseOrder.AnyAsync(m => m.DropShipToCustomerId == inObj.Id)) - // AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PurchaseOrder")); + if (await ct.WorkOrder.AnyAsync(m => m.FromPMId == dbObject.Id)) + AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PM")); } - //############### NOTIFICATION TODO - /* - todo: quote notifications remove #30 and #32 as redundant - WorkorderStatusChange = 4,//* Workorder object, any *change* of status including from no status (new) to a specific conditional status ID value - - WorkorderStatusAge = 24,//* Workorder object Created / Updated, conditional on exact status selected IdValue, Tags conditional, advance notice can be set - - //THESE TWO ARE REDUNDANT: - - this is actually workorderstatuschange because can just pick any status under workorderstatuschange to be notified about - WorkorderCompleted = 30, //*travel work order is set to any status that is flagged as a "Completed" type of status. Customer & User - - //This one could be accomplished with WorkorderStatusAge, just pick a Completed status and set a time frame and wala! - WorkorderCompletedFollowUp = 32, //* travel quote closed status follow up again after this many TIMESPAN - - todo: CHANGE WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged quote status type in selected time span from creation of quote - Change this to a new type that is based on so many days *without* being set to a particular status - but first check if tied to contract response time stuff, how that's handled - that's closeby date in v7 but isn't that deprecated now without a "close"? - maybe I do need the Completed status bool thing above - - */ //////////////////////////////////////////////////////////////////////////////////////////////// @@ -932,64 +800,6 @@ namespace AyaNova.Biz foreach (long batchId in batch) batchResults.Add(await PMGetPartialAsync(dataListSelectedRequest.AType, batchId, dataListSelectedRequest.IncludeWoItemDescendants, true)); - #region unnecessary shit removed - //This is unnecessary because the re-ordering bit is only needed when the records are fetched in batches directly from the sql server as they - //return in db natural order and need to be put back into the same order as the ID List - //Here in the quote however, this code is fetching individually one at a time so they are always going to be in the correct order so this re-ordering is unnecessary - //I'm keeping this here for future reference when I ineveitably wonder what the hell is happening here :) - - - //order the results back into original - //IEnumerable orderedList = null; - - //TODO: WHAT IS THIS BATCH RESULT ORDERING CODE REALLY DOING AND CAN IT BE REMOVED / CHANGED???? - //isn't it alredy working in order? If not maybe simply reversed so reverse it again before querying above or...?? - - //todo: can't assume the grandchild item is index 0 anymore as we might have multiple of them if includedescendants is true - //so need to find index first then do this - // switch (dataListSelectedRequest.AType) - // { - // case AyaType.PM: - // orderedList = from id in batch join z in batchResults on id equals z.Id select z; - // break; - // case AyaType.PMItem: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].Id select z; - // break; - // case AyaType.PMItemExpense: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].Expenses[0].Id select z; - // break; - // case AyaType.PMItemLabor: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].Labors[0].Id select z; - // break; - // case AyaType.PMItemLoan: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].Loans[0].Id select z; - // break; - // case AyaType.PMItemPart: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].Parts[0].Id select z; - // break; - // case AyaType.PMItemPartRequest: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].PartRequests[0].Id select z; - // break; - // case AyaType.PMItemScheduledUser: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].ScheduledUsers[0].Id select z; - // break; - // case AyaType.PMItemTask: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].Tasks[0].Id select z; - // break; - // case AyaType.PMItemTravel: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].Travels[0].Id select z; - // break; - // case AyaType.PMItemOutsideService: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].OutsideServices[0].Id select z; - // break; - // case AyaType.PMItemUnit: - // orderedList = from id in batch join z in batchResults on id equals z.Items[0].Units[0].Id select z; - // break; - // } - - //foreach (PM w in orderedList) - #endregion unnecessary shit - foreach (PM w in batchResults) { var jo = JObject.FromObject(w); @@ -1070,8 +880,6 @@ namespace AyaNova.Biz else o.ContractViz = "-"; - - } @@ -1086,31 +894,6 @@ namespace AyaNova.Biz return await GetReportData(dataListSelectedRequest); } - // public async Task> ImportData(JArray ja) - // { - // List ImportResult = new List(); - // string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}"; - - // var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) }); - // foreach (JObject j in ja) - // { - // var w = j.ToObject(jsset); - // if (j["CustomFields"] != null) - // w.CustomFields = j["CustomFields"].ToString(); - // w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary - // var res = await PMCreateAsync(w); - // if (res == null) - // { - // ImportResult.Add($"* {w.Serial} - {this.GetErrorsAsString()}"); - // this.ClearErrors(); - // } - // else - // { - // ImportResult.Add($"{w.Serial} - ok"); - // } - // } - // return ImportResult; - // } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS @@ -1581,7 +1364,7 @@ namespace AyaNova.Biz var qid = await GetPMIdFromRelativeAsync(AyaType.PMItem, oProposed.PMId, ct); var WorkorderInfo = await ct.PM.AsNoTracking().Where(x => x.Id == qid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name field itself - if (WorkorderInfo != null) + if (WorkorderInfo != null) oProposed.Name = WorkorderInfo.Serial.ToString(); else oProposed.Name = "??"; diff --git a/server/AyaNova/biz/QuoteBiz.cs b/server/AyaNova/biz/QuoteBiz.cs index 1cc54ed8..f4227abc 100644 --- a/server/AyaNova/biz/QuoteBiz.cs +++ b/server/AyaNova/biz/QuoteBiz.cs @@ -350,7 +350,7 @@ namespace AyaNova.Biz AddError(ApiErrorCode.NOT_FOUND); return false; } - QuoteValidateCanDelete(dbObject); + await QuoteValidateCanDelete(dbObject); if (HasErrors) return false; @@ -493,38 +493,6 @@ namespace AyaNova.Biz } } - - - // //////////////////////////////////////////////////////////////////////////////////////////////// - // //CONTRACT UPDATE - // // - // internal async Task ChangeContract(long workOrderId, long? newContractId) - // { - // //this is called by UI via contract change route for contract change only and expects wo back to update client ui - // var w = await ct.Quote.FirstOrDefaultAsync(z => z.Id == workOrderId); - - // if (w == null) - // { - // AddError(ApiErrorCode.NOT_FOUND, "id"); - // return null; - // } - // if (newContractId != null && !await ct.Contract.AnyAsync(z => z.Id == newContractId)) - // { - // AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"Contract with id {newContractId} not found"); - // return null; - // } - // w.ContractId = newContractId; - // await ct.SaveChangesAsync(); - // await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, workOrderId, BizType, AyaEvent.Modified), ct); - // await GetCurrentContractFromContractIdAsync(newContractId); - // var updatedQuote = await ProcessChangeOfContractAsync(workOrderId); - // await QuotePopulateVizFields(updatedQuote, false); - // return updatedQuote;//return entire quote - // } - - - - //////////////////////////////////////////////////////////////////////////////////////////////// //GET WORKORDER ID FROM DESCENDANT TYPE AND ID // @@ -587,20 +555,6 @@ namespace AyaNova.Biz } - - // //////////////////////////////////////////////////////////////////////////////////////////////// - // //GET WORKORDER ID FOR WORK ORDER NUMBER - // // - // internal static async Task GetQuoteIdForNumberAsync(long woNumber, AyContext ct) - // { - // return await ct.Quote.AsNoTracking() - // .Where(z => z.Serial == woNumber) - // .Select(z => z.Id) - // .SingleOrDefaultAsync(); - // } - - - //////////////////////////////////////////////////////////////////////////////////////////////// //SEARCH // @@ -783,7 +737,7 @@ namespace AyaNova.Biz - private void QuoteValidateCanDelete(Quote dbObject) + private async Task QuoteValidateCanDelete(Quote dbObject) { //Check restricted role preventing create if (UserIsRestrictedType) @@ -791,16 +745,9 @@ namespace AyaNova.Biz AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } - //FOREIGN KEY CHECKS - //these are examples copied from customer for when other objects are actually referencing them - // if (await ct.User.AnyAsync(m => m.CustomerId == inObj.Id)) - // AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User")); - // if (await ct.Unit.AnyAsync(m => m.CustomerId == inObj.Id)) - // AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Unit")); - // if (await ct.CustomerServiceRequest.AnyAsync(m => m.CustomerId == inObj.Id)) - // AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("CustomerServiceRequest")); - // if (await ct.PurchaseOrder.AnyAsync(m => m.DropShipToCustomerId == inObj.Id)) - // AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PurchaseOrder")); + if (await ct.WorkOrder.AnyAsync(m => m.FromQuoteId == dbObject.Id)) + AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Quote")); + } @@ -1380,239 +1327,189 @@ namespace AyaNova.Biz log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; - QuoteState oProposed = (QuoteState)proposedObj; - var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == oProposed.QuoteId).Select(x => new { x.Serial, x.Tags, x.CustomerId }).FirstOrDefaultAsync(); - QuoteStatus wos = await ct.QuoteStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.QuoteStatusId); - //for notification purposes because has no name / tags field itself - oProposed.Name = WorkorderInfo.Serial.ToString(); - oProposed.Tags = WorkorderInfo.Tags; + //currently no quote state notifications but may well be in future so this saves changing a bunch of shit if necessary later + await Task.CompletedTask; + // QuoteState oProposed = (QuoteState)proposedObj; - //STANDARD EVENTS FOR ALL OBJECTS - //NONE: state notifications are specific and not the same as for general objects so don't process standard events + // var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == oProposed.QuoteId).Select(x => new { x.Serial, x.Tags, x.CustomerId }).FirstOrDefaultAsync(); + // QuoteStatus wos = await ct.QuoteStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.QuoteStatusId); + // //for notification purposes because has no name / tags field itself + // oProposed.Name = WorkorderInfo.Serial.ToString(); + // oProposed.Tags = WorkorderInfo.Tags; - //SPECIFIC EVENTS FOR THIS OBJECT - //WorkorderStatusChange = 4,//*Workorder object, any NEW status set. Conditions: specific status ID value only (no generic any status allowed), Workorder TAGS - //WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged quote status type in selected time span from creation of workorderWorkorderSetToCompletedStatus - //WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set + // //STANDARD EVENTS FOR ALL OBJECTS + // //NONE: state notifications are specific and not the same as for general objects so don't process standard events - //NOTE: ID, state notifications are for the Workorder, not the state itself unlike other objects, so use the WO type and ID here for all notifications + // //SPECIFIC EVENTS FOR THIS OBJECT + // //WorkorderStatusChange = 4,//*Workorder object, any NEW status set. Conditions: specific status ID value only (no generic any status allowed), Workorder TAGS + // //WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged quote status type in selected time span from creation of workorderWorkorderSetToCompletedStatus + // //WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set + + // //NOTE: ID, state notifications are for the Workorder, not the state itself unlike other objects, so use the WO type and ID here for all notifications - //## DELETED EVENTS - //A state cannot be deleted so nothing to handle that is required - //a quote CAN be deleted and it will automatically remove all events for it so also no need to remove time delayed status events either if wo is deleted. - //so in essence there is nothing to be done regarding deleted events with states in a blanket way, however specific events below may remove them as appropriate + // //## DELETED EVENTS + // //A state cannot be deleted so nothing to handle that is required + // //a quote CAN be deleted and it will automatically remove all events for it so also no need to remove time delayed status events either if wo is deleted. + // //so in essence there is nothing to be done regarding deleted events with states in a blanket way, however specific events below may remove them as appropriate - // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusChange); - // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderCompletedStatusOverdue); - // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusAge); + // // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusChange); + // // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderCompletedStatusOverdue); + // // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusAge); - //## CREATED (this is the only possible notification CREATION ayaEvent type for a quote state as they are create only) - if (ayaEvent == AyaEvent.Created) - { - //# STATUS CHANGE (create new status) - { - //Conditions: must match specific status id value and also tags below - //delivery is immediate so no need to remove old ones of this kind - var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusChange && z.IdValue == oProposed.QuoteStatusId).ToListAsync(); - foreach (var sub in subs) - { - //not for inactive users - if (!await UserBiz.UserIsActive(sub.UserId)) continue; + // //## CREATED (this is the only possible notification CREATION ayaEvent type for a quote state as they are create only) + // if (ayaEvent == AyaEvent.Created) + // { + // //# STATUS CHANGE (create new status) + // { + // //Conditions: must match specific status id value and also tags below + // //delivery is immediate so no need to remove old ones of this kind + // var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusChange && z.IdValue == oProposed.QuoteStatusId).ToListAsync(); + // foreach (var sub in subs) + // { + // //not for inactive users + // if (!await UserBiz.UserIsActive(sub.UserId)) continue; - //Tag match? (will be true if no sub tags so always safe to call this) - if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) - { - NotifyEvent n = new NotifyEvent() - { - EventType = NotifyEventType.WorkorderStatusChange, - UserId = sub.UserId, - AyaType = AyaType.Quote, - ObjectId = oProposed.QuoteId, - NotifySubscriptionId = sub.Id, - Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}" - }; - await ct.NotifyEvent.AddAsync(n); - log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - await ct.SaveChangesAsync(); - } - } - }//quote status change event + // //Tag match? (will be true if no sub tags so always safe to call this) + // if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) + // { + // NotifyEvent n = new NotifyEvent() + // { + // EventType = NotifyEventType.WorkorderStatusChange, + // UserId = sub.UserId, + // AyaType = AyaType.Quote, + // ObjectId = oProposed.QuoteId, + // NotifySubscriptionId = sub.Id, + // Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}" + // }; + // await ct.NotifyEvent.AddAsync(n); + // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); + // await ct.SaveChangesAsync(); + // } + // } + // }//quote status change event - //# STATUS AGE - { - //WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set - //Always clear any old ones for this object as they are all irrelevant the moment the state has changed: - await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.WorkorderStatusAge); - var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusAge && z.IdValue == oProposed.QuoteStatusId).ToListAsync(); - foreach (var sub in subs) - { - //not for inactive users - if (!await UserBiz.UserIsActive(sub.UserId)) continue; + // //# STATUS AGE + // { + // //WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set + // //Always clear any old ones for this object as they are all irrelevant the moment the state has changed: + // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.WorkorderStatusAge); + // var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusAge && z.IdValue == oProposed.QuoteStatusId).ToListAsync(); + // foreach (var sub in subs) + // { + // //not for inactive users + // if (!await UserBiz.UserIsActive(sub.UserId)) continue; - //Quote Tag match? (Not State, state has no tags, will be true if no sub tags so always safe to call this) - if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) - { - NotifyEvent n = new NotifyEvent() - { - EventType = NotifyEventType.WorkorderStatusAge, - UserId = sub.UserId, - AyaType = AyaType.Quote, - ObjectId = oProposed.QuoteId, - NotifySubscriptionId = sub.Id, - Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}" - }; - await ct.NotifyEvent.AddAsync(n); - log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - await ct.SaveChangesAsync(); - } - } - }//quote status change event + // //Quote Tag match? (Not State, state has no tags, will be true if no sub tags so always safe to call this) + // if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) + // { + // NotifyEvent n = new NotifyEvent() + // { + // EventType = NotifyEventType.WorkorderStatusAge, + // UserId = sub.UserId, + // AyaType = AyaType.Quote, + // ObjectId = oProposed.QuoteId, + // NotifySubscriptionId = sub.Id, + // Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}" + // }; + // await ct.NotifyEvent.AddAsync(n); + // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); + // await ct.SaveChangesAsync(); + // } + // } + // }//quote status change event - //# COMPLETE BY OVERDUE - { - //NOTE: the initial notification is created by the Workorder Header notification as it's where this time delayed notification is first generated - //the only job here in state notification is to remove any prior finish overdue notifications waiting if a new state is selected that is a completed state + // //# COMPLETE BY OVERDUE + // { + // //NOTE: the initial notification is created by the Workorder Header notification as it's where this time delayed notification is first generated + // //the only job here in state notification is to remove any prior finish overdue notifications waiting if a new state is selected that is a completed state - //NOTE ABOUT RE-OPEN DECISION ON HOW THIS WORKS: + // //NOTE ABOUT RE-OPEN DECISION ON HOW THIS WORKS: - //what though if it's not a Completed status, then I guess don't remove it, but what if it *was* a Completed status and it's change to a non Completed? - //that, in essence re-opens it so it's not Completed at that point. - //My decision on this june 2021 is that a work order Completed status notification is satisifed the moment it's saved with a Completed status - //and nothing afterwards restarts that process so if a person sets closed status then sets open status again no new Completed overdue notification will be generated + // //what though if it's not a Completed status, then I guess don't remove it, but what if it *was* a Completed status and it's change to a non Completed? + // //that, in essence re-opens it so it's not Completed at that point. + // //My decision on this june 2021 is that a work order Completed status notification is satisifed the moment it's saved with a Completed status + // //and nothing afterwards restarts that process so if a person sets closed status then sets open status again no new Completed overdue notification will be generated - if (wos.Completed) - { - //Workorder was just set to a completed status so remove any notify events lurking to deliver for overdue - await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, oProposed.QuoteId, NotifyEventType.WorkorderCompletedStatusOverdue); - } - }//quote complete by overdue change event + // if (wos.Completed) + // { + // //Workorder was just set to a completed status so remove any notify events lurking to deliver for overdue + // await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, oProposed.QuoteId, NotifyEventType.WorkorderCompletedStatusOverdue); + // } + // }//quote complete by overdue change event - //# WorkorderTotalExceedsThreshold / "The Andy" - { - if (wos.Completed) - { + - //see if any subscribers to the quote total exceeds notification - //that are active then proceed to fetch billed woitem children and total quote and send notification if necessary + // //# WorkorderCompleted - Customer AND User but customer only notifies if it's their quote + // { + // if (wos.Completed) + // { + // //look for potential subscribers + // var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompleted).ToListAsync(); + // foreach (var sub in subs) + // { + // //not for inactive users + // if (!await UserBiz.UserIsActive(sub.UserId)) continue; - bool haveTotal = false; - decimal GrandTotal = 0m; + // //Customer User? + // var UserInfo = await ct.User.AsNoTracking().Where(x => x.Id == sub.UserId).Select(x => new { x.CustomerId, x.UserType, x.HeadOfficeId }).FirstOrDefaultAsync(); + // if (UserInfo.UserType == UserType.Customer || UserInfo.UserType == UserType.HeadOffice) + // { + // //CUSTOMER USER - //look for potential subscribers - var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderTotalExceedsThreshold).ToListAsync(); - foreach (var sub in subs) - { - //not for inactive users - if (!await UserBiz.UserIsActive(sub.UserId)) continue; + // //Quick short circuit: if quote doesn't have a customer id then it's not going to match no matter what + // if (WorkorderInfo.CustomerId == 0) continue; - //Tag match? (will be true if no sub tags so always safe to call this) - //check early to avoid cost of fetching and calculating total if unnecessary - if (!NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) continue; + // var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId); - //get the total because we have at least one subscriber and matching tags - if (haveTotal == false) - { - GrandTotal = await WorkorderGrandTotalAsync(oProposed.QuoteId, ct); - haveTotal = true; + // //Are they allowed right now to use this type of notification? + // if (!customerUserRights.NotifyWOCompleted) continue; - //Note: not a time delayed notification, however user could be flipping states quickly triggering multiple notifications that are in queue temporarily - //so this will prevent that: - await NotifyEventHelper.ClearPriorEventsForObject(ct, AyaType.Quote, oProposed.QuoteId, NotifyEventType.WorkorderTotalExceedsThreshold); - } - //Ok, we're here because there is a subscriber who is active and tags match so only check left is total against decvalue - if (sub.DecValue < GrandTotal) - { - //notification is a go - NotifyEvent n = new NotifyEvent() - { - EventType = NotifyEventType.WorkorderTotalExceedsThreshold, - UserId = sub.UserId, - AyaType = AyaType.Quote, - ObjectId = oProposed.QuoteId, - NotifySubscriptionId = sub.Id, - Name = $"{WorkorderInfo.Serial.ToString()}", - DecValue = GrandTotal - }; - await ct.NotifyEvent.AddAsync(n); - log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - await ct.SaveChangesAsync(); - } - } - } - }//"The Andy" for Dynamic Dental corp. notification + // //is this their related work order? + // if (UserInfo.CustomerId != WorkorderInfo.CustomerId) + // { + // //not the same customer but might be a head office user and this is one of their customers so check for that + // if (UserInfo.HeadOfficeId == null) continue;//can't match any head office so no need to go further + // //see if quote customer's head office is the same id as the user's headofficeid (note that a customer user with the same head office as a *different* customer quote doesn't qualify) + // var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == WorkorderInfo.CustomerId).Select(x => new { x.HeadOfficeId, x.BillHeadOffice }).FirstOrDefaultAsync(); + // if (!CustomerInfo.BillHeadOffice) continue;//can't possibly match so no need to go further + // if (UserInfo.HeadOfficeId != CustomerInfo.HeadOfficeId) continue; + // } + // } + // else + // { + // //INSIDE USER + // //Tag match? (will be true if no sub tags so always safe to call this) + // //check early to avoid cost of fetching and calculating total if unnecessary + // if (!NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) continue; + // } - //# WorkorderCompleted - Customer AND User but customer only notifies if it's their quote - { - if (wos.Completed) - { - //look for potential subscribers - var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompleted).ToListAsync(); - foreach (var sub in subs) - { - //not for inactive users - if (!await UserBiz.UserIsActive(sub.UserId)) continue; - - //Customer User? - var UserInfo = await ct.User.AsNoTracking().Where(x => x.Id == sub.UserId).Select(x => new { x.CustomerId, x.UserType, x.HeadOfficeId }).FirstOrDefaultAsync(); - if (UserInfo.UserType == UserType.Customer || UserInfo.UserType == UserType.HeadOffice) - { - //CUSTOMER USER - - //Quick short circuit: if quote doesn't have a customer id then it's not going to match no matter what - if (WorkorderInfo.CustomerId == 0) continue; - - var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId); - - //Are they allowed right now to use this type of notification? - if (!customerUserRights.NotifyWOCompleted) continue; - - //is this their related work order? - if (UserInfo.CustomerId != WorkorderInfo.CustomerId) - { - //not the same customer but might be a head office user and this is one of their customers so check for that - if (UserInfo.HeadOfficeId == null) continue;//can't match any head office so no need to go further - - //see if quote customer's head office is the same id as the user's headofficeid (note that a customer user with the same head office as a *different* customer quote doesn't qualify) - var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == WorkorderInfo.CustomerId).Select(x => new { x.HeadOfficeId, x.BillHeadOffice }).FirstOrDefaultAsync(); - if (!CustomerInfo.BillHeadOffice) continue;//can't possibly match so no need to go further - if (UserInfo.HeadOfficeId != CustomerInfo.HeadOfficeId) continue; - } - } - else - { - //INSIDE USER - //Tag match? (will be true if no sub tags so always safe to call this) - //check early to avoid cost of fetching and calculating total if unnecessary - if (!NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) continue; - } - - //Ok, we're here so it must be ok to notify user - NotifyEvent n = new NotifyEvent() - { - EventType = NotifyEventType.WorkorderCompleted, - UserId = sub.UserId, - AyaType = AyaType.Quote, - ObjectId = oProposed.QuoteId, - NotifySubscriptionId = sub.Id, - Name = $"{WorkorderInfo.Serial.ToString()}" - }; - await ct.NotifyEvent.AddAsync(n); - log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); - await ct.SaveChangesAsync(); - } - } - }//WorkorderCompleted - } + // //Ok, we're here so it must be ok to notify user + // NotifyEvent n = new NotifyEvent() + // { + // EventType = NotifyEventType.WorkorderCompleted, + // UserId = sub.UserId, + // AyaType = AyaType.Quote, + // ObjectId = oProposed.QuoteId, + // NotifySubscriptionId = sub.Id, + // Name = $"{WorkorderInfo.Serial.ToString()}" + // }; + // await ct.NotifyEvent.AddAsync(n); + // log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); + // await ct.SaveChangesAsync(); + // } + // } + // }//WorkorderCompleted + // } }//end of process notifications @@ -1981,7 +1878,7 @@ namespace AyaNova.Biz var qid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteId, ct); var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == qid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name field itself - if (WorkorderInfo != null) + if (WorkorderInfo != null) oProposed.Name = WorkorderInfo.Serial.ToString(); else oProposed.Name = "??";