This commit is contained in:
2021-07-30 21:11:09 +00:00
parent a10c30108b
commit 58b05dd1f3
2 changed files with 163 additions and 483 deletions

View File

@@ -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<decimal> 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<PM> 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<List<string>> ImportData(JArray ja)
// {
// List<string> ImportResult = new List<string>();
// 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<PM>(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 = "??";

View File

@@ -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<Quote> 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<long> 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 = "??";