using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Microsoft.EntityFrameworkCore.Storage; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; using System.Linq; using System; using Newtonsoft.Json.Linq; using System.Collections.Generic; namespace AyaNova.Biz { internal class WorkOrderBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject { internal WorkOrderBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles, UserType currentUserType) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = UserRoles; BizType = AyaType.WorkOrder; CurrentUserType = currentUserType; //Sub-role rights flags UserIsTechRestricted = CurrentUserRoles.HasFlag(AuthorizationRoles.TechRestricted); UserIsSubContractorFull = CurrentUserType == UserType.ServiceContractor && CurrentUserRoles.HasFlag(AuthorizationRoles.SubContractor); UserIsSubContractorRestricted = CurrentUserType == UserType.ServiceContractor && CurrentUserRoles.HasFlag(AuthorizationRoles.SubContractorRestricted); UserIsRestrictedType = UserIsTechRestricted || UserIsSubContractorFull || UserIsSubContractorRestricted; UserCanViewPartCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.InventoryRestricted) || CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory) || CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) || CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting); UserCanViewLaborOrTravelRateCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.Service) || CurrentUserRoles.HasFlag(AuthorizationRoles.ServiceRestricted) || CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) || CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting); UserCanViewLoanerCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.Service) || CurrentUserRoles.HasFlag(AuthorizationRoles.ServiceRestricted) || CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin) || CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting); } internal static WorkOrderBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) { if (httpContext != null) return new WorkOrderBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items), UserTypeFromContext.Type(httpContext.Items)); else return new WorkOrderBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin, UserType.Service); } /* ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗ ██║ █╗ ██║██║ ██║██████╔╝█████╔╝█████╗██║ ██║██████╔╝██║ ██║█████╗ ██████╔╝ ██║███╗██║██║ ██║██╔══██╗██╔═██╗╚════╝██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ ╚██████╔╝██║ ██║██████╔╝███████╗██║ ██║ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝ */ #region WorkOrder level //////////////////////////////////////////////////////////////////////////////////////////////// // SUBRIGHTS / RESTRICTIONS FOR WORK ORDER // //Note: these restrictions and rights are in addition to the basic fundamental role access rights (layer 1) //and are considered after role rights have already been consulted first (usually at the controller level) internal UserType CurrentUserType { get; set; } internal bool UserIsRestrictedType { get; set; } internal bool UserIsTechRestricted { get; set; } internal bool UserIsSubContractorFull { get; set; } internal bool UserIsSubContractorRestricted { get; set; } internal bool UserCanViewPartCosts { get; set; } internal bool UserCanViewLaborOrTravelRateCosts { get; set; } internal bool UserCanViewLoanerCosts { get; set; } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task WorkOrderExistsAsync(long id) { return await ct.WorkOrder.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task WorkOrderCreateAsync(WorkOrder newObject, bool populateViz = true) { using (var transaction = await ct.Database.BeginTransactionAsync()) { await WorkOrderValidateAsync(newObject, null); if (HasErrors) return null; else { await WorkOrderBizActionsAsync(AyaEvent.Created, newObject, null, null); newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrder.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await WorkOrderSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); //# NOTE: only internal code can post an entire workorder graph, no external user can as controller will reject right up front //however, internally seeder will post entire workorders if (newObject.Items.Count > 0) { await GetCurrentContractFromContractIdAsync(newObject.ContractId); if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero) newObject.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime); //GRANDCHILD BIZ ACTIONS foreach (WorkOrderItem wi in newObject.Items) { foreach (WorkOrderItemPart wip in wi.Parts) await PartBizActionsAsync(AyaEvent.Created, wip, null); foreach (WorkOrderItemLoan wil in wi.Loans) await LoanBizActionsAsync(AyaEvent.Created, wil, null); } await ct.SaveChangesAsync(); //INVENTORY ADJUSTMENTS foreach (WorkOrderItem wi in newObject.Items) { foreach (WorkOrderItemPart wip in wi.Parts) { await PartInventoryAdjustmentAsync(AyaEvent.Created, wip, null, transaction); if (HasErrors) { await transaction.RollbackAsync(); return null; } } } //NOTE: not running individual notification here for children, seeder won't require it and that's all that posts an entire wo currently } await transaction.CommitAsync(); if (populateViz) await WorkOrderPopulateVizFields(newObject, true, false); if (newObject.GenCopyAttachmentsFrom != null && !newObject.GenCopyAttachmentsFrom.IsEmpty) { //copy attachment from existing object await AttachmentBiz.DuplicateAttachments(newObject.GenCopyAttachmentsFrom, new AyaTypeId(AyaType.WorkOrder, newObject.Id), ct); newObject.GenCopyAttachmentsFrom = null;//so it doesn't get returned } await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } } //workorder needs to be fetched internally from several places for rule checking etc //this just gets it raw and lets others process private async Task WorkOrderGetFullAsync(long id) { //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections return await ct.WorkOrder.AsSplitQuery().AsNoTracking() .Include(s => s.States) .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.PartRequests) .Include(w => w.Items) .ThenInclude(wi => wi.ScheduledUsers) .Include(w => w.Items) .ThenInclude(wi => wi.Tasks.OrderBy(t => t.Sequence)) .Include(w => w.Items) .ThenInclude(wi => wi.Travels) .Include(w => w.Items) .ThenInclude(wi => wi.Units) .Include(w => w.Items) .ThenInclude(wi => wi.OutsideServices) .SingleOrDefaultAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task WorkOrderGetAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true, bool populateForReporting = false) { var ret = await WorkOrderGetFullAsync(id); if (ret != null) { var stat = await GetCurrentWorkOrderStatusFromRelatedAsync(BizType, ret.Id); ret.IsLockedAtServer = stat.Locked; var userIsTechRestricted = UserIsTechRestricted; var userIsSubContractorFull = UserIsSubContractorFull; var userIsSubContractorRestricted = UserIsSubContractorRestricted; var userIsRestricted = (userIsTechRestricted || userIsSubContractorFull || userIsSubContractorRestricted); if (userIsRestricted) { //Restricted users can only work with workorder items they are scheduled on List removeItems = new List(); //gather list of items to remove by checking if they are scheduled on them or not foreach (WorkOrderItem wi in ret.Items) { var userIsSelfScheduledOnThisItem = false; foreach (WorkOrderItemScheduledUser su in wi.ScheduledUsers) { if (su.UserId == UserId) { userIsSelfScheduledOnThisItem = true; break; } } if (!userIsSelfScheduledOnThisItem) removeItems.Add(wi); } foreach (var removeitem in removeItems) { ret.Items.Remove(removeitem); ret.IsCompleteRecord = false; } //Restricted users may have further restrictions foreach (WorkOrderItem wi in ret.Items) { //all restricted types wi.ScheduledUsers.RemoveAll(x => x.UserId != UserId); wi.Labors.RemoveAll(x => x.UserId != UserId); wi.Travels.RemoveAll(x => x.UserId != UserId); if (userIsTechRestricted) { wi.Expenses.RemoveAll(x => x.UserId != UserId); } if (userIsSubContractorFull) { wi.Expenses.RemoveAll(x => true); wi.OutsideServices.RemoveAll(x => true); } if (userIsSubContractorRestricted) { wi.Units.RemoveAll(x => true); wi.Parts.RemoveAll(x => true); wi.PartRequests.RemoveAll(x => true); wi.Expenses.RemoveAll(x => true); wi.Loans.RemoveAll(x => true); wi.OutsideServices.RemoveAll(x => true); } //tasks are allowed to be viewed and update the task completion types } } if (populateDisplayFields) await WorkOrderPopulateVizFields(ret, false, populateForReporting); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); } return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task WorkOrderPutAsync(WorkOrder putObject) { //## PUT HEADER ONLY, NO ALLOWANCE FOR PUT OF ENTIRE WORKORDER //Note: this is intentionally not using the getasync because //doing so would invoke the children which would then get deleted on save since putobject has no children WorkOrder dbObject = await ct.WorkOrder.AsNoTracking().FirstOrDefaultAsync(z => z.Id == putObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } putObject.Tags = TagBiz.NormalizeTags(putObject.Tags); putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields); await WorkOrderValidateAsync(putObject, dbObject); if (HasErrors) return null; await WorkOrderBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null); long? newContractId = null; if (putObject.ContractId != dbObject.ContractId)//manual change of contract { newContractId = putObject.ContractId; await GetCurrentContractFromContractIdAsync(newContractId); if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero) putObject.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime); } ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await WorkOrderExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); await WorkOrderSearchIndexAsync(putObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); await WorkOrderPopulateVizFields(putObject, true, false); await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task WorkOrderDeleteAsync(long id) { using (var transaction = await ct.Database.BeginTransactionAsync()) { WorkOrder dbObject = await ct.WorkOrder.AsNoTracking().Where(z => z.Id == id).FirstOrDefaultAsync();// WorkOrderGetAsync(id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND); return false; } WorkOrderValidateCanDelete(dbObject); if (HasErrors) return false; //States collection if (!await StateDeleteAsync(id, transaction)) return false; //collect the child id's to delete var ItemIds = await ct.WorkOrderItem.AsNoTracking().Where(z => z.WorkOrderId == id).Select(z => z.Id).ToListAsync(); //Delete children foreach (long ItemId in ItemIds) if (!await ItemDeleteAsync(ItemId, transaction)) return false; ct.WorkOrder.Remove(dbObject); await ct.SaveChangesAsync(); await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, dbObject.Serial.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct); await transaction.CommitAsync(); await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } } //////////////////////////////////////////////////////////////////////////////////////////////// //BIZ ACTIONS // // private async Task WorkOrderBizActionsAsync(AyaEvent ayaEvent, WorkOrder newObj, WorkOrder oldObj, IDbContextTransaction transaction) { //automatic actions on record change, called AFTER validation and BEFORE save //so changes here will be saved by caller //currently no processing required except for created or modified at this time if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified) return; //CREATED OR MODIFIED if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified) { } //CREATION ACTIONS if (ayaEvent == AyaEvent.Created) { await AutoSetContractAsync(newObj); if (newObj.CompleteByDate == null)//need to account for a user manually selecting a specific close by date in advance indicating to ignore any auto sets await AutoSetCloseByDateAsync(newObj); await AutoSetAddressAsync(newObj); } //MODIFIED ACTIONS if (ayaEvent == AyaEvent.Modified) { //if customer changed then contractId must be re-checked if (newObj.CustomerId != oldObj.CustomerId) { await AutoSetContractAsync(newObj); await AutoSetAddressAsync(newObj); } if (newObj.ContractId != oldObj.ContractId) await AutoSetCloseByDateAsync(newObj); } } private async Task AutoSetAddressAsync(WorkOrder newObj) { if (newObj.CustomerId == 0) return; var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).FirstOrDefaultAsync(); if (cust == null) return; newObj.PostAddress = cust.PostAddress; newObj.PostCity = cust.PostCity; newObj.PostRegion = cust.PostRegion; newObj.PostCountry = cust.PostCountry; newObj.PostCode = cust.PostCode; newObj.Address = cust.Address; newObj.City = cust.City; newObj.Region = cust.Region; newObj.Country = cust.Country; newObj.Latitude = cust.Latitude; newObj.Longitude = cust.Longitude; if (cust.BillHeadOffice == true && cust.HeadOfficeId != null) { var head = await ct.HeadOffice.AsNoTracking().Where(z => z.Id == cust.HeadOfficeId).FirstOrDefaultAsync(); if (head == null) return; newObj.PostAddress = head.PostAddress; newObj.PostCity = head.PostCity; newObj.PostRegion = head.PostRegion; newObj.PostCountry = head.PostCountry; newObj.PostCode = head.PostCode; } } private async Task AutoSetContractAsync(WorkOrder newObj) { //first reset contract fetched flag so a fresh copy is taken //in case it was set already by other operations mFetchedContractAlready = false; //CONTRACT AUTO SET //failsafe newObj.ContractId = null; if (newObj.CustomerId != 0) { //precedence: unit->customer->headoffice var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).Select(z => new { headofficeId = z.HeadOfficeId, contractId = z.ContractId, contractExpires = z.ContractExpires }).FirstOrDefaultAsync(); //first set it to the customer one if available in case the ho one has expired then set the ho if applicable if (cust.contractId != null && cust.contractExpires > DateTime.UtcNow) newObj.ContractId = cust.contractId; else if (cust.headofficeId != null) { var head = await ct.HeadOffice.AsNoTracking().Where(z => z.Id == cust.headofficeId).Select(z => new { contractId = z.ContractId, contractExpires = z.ContractExpires }).FirstOrDefaultAsync(); if (head.contractId != null && head.contractExpires > DateTime.UtcNow) newObj.ContractId = head.contractId; } } } private async Task AutoSetCloseByDateAsync(WorkOrder newObj) { //called when there is a definite possibility of change of close by i.e. new contract, new customer, new workorder //RESPONSE TIME / COMPLETE BY AUTO SET //precedence: manually pre-set -> contract -> global biz if (newObj.ContractId != null) { await GetCurrentContractFromContractIdAsync(newObj.ContractId); if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero) { newObj.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime); return; //our work here is done } } //not set yet, maybe the global default is the way... if (AyaNova.Util.ServerGlobalBizSettings.Cache.WorkOrderCompleteByAge != TimeSpan.Zero) newObj.CompleteByDate = DateTime.UtcNow.Add(AyaNova.Util.ServerGlobalBizSettings.Cache.WorkOrderCompleteByAge); } //////////////////////////////////////////////////////////////////////////////////////////////// //GET WORKORDER ID FROM DESCENDANT TYPE AND ID // internal static async Task GetWorkOrderIdFromRelativeAsync(AyaType ayaType, long id, AyContext ct) { ParentAndChildItemId w = new ParentAndChildItemId(); long woitemid = 0; switch (ayaType) { case AyaType.WorkOrder: w.ParentId = id; w.ChildItemId = 0; return w; case AyaType.WorkOrderItem: woitemid = id; break; case AyaType.WorkOrderItemExpense: woitemid = await ct.WorkOrderItemExpense.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemLabor: woitemid = await ct.WorkOrderItemLabor.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemLoan: woitemid = await ct.WorkOrderItemLoan.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemPart: woitemid = await ct.WorkOrderItemPart.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemPartRequest: woitemid = await ct.WorkOrderItemPartRequest.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemScheduledUser: woitemid = await ct.WorkOrderItemScheduledUser.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemTask: woitemid = await ct.WorkOrderItemTask.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemTravel: woitemid = await ct.WorkOrderItemTravel.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderItemOutsideService: woitemid = await ct.WorkOrderItemOutsideService.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; case AyaType.WorkOrderStatus: w.ParentId = await ct.WorkOrderState.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderId).SingleOrDefaultAsync(); w.ChildItemId = 0; return w; case AyaType.WorkOrderItemUnit: woitemid = await ct.WorkOrderItemUnit.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync(); break; default: throw new System.NotSupportedException($"WorkOrderBiz::GetWorkOrderIdFromRelativeAsync -> AyaType {ayaType.ToString()} is not supported"); } w.ParentId = await ct.WorkOrderItem.AsNoTracking() .Where(z => z.Id == woitemid) .Select(z => z.WorkOrderId) .SingleOrDefaultAsync(); w.ChildItemId = woitemid; return w; } //////////////////////////////////////////////////////////////////////////////////////////////// //GET WORKORDER SERIAL FROM DESCENDANT TYPE AND ID // internal static async Task GetWorkOrderSerialFromRelativeAsync(AyaType ayaType, long id, AyContext ct) { var wid = (await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct)).ParentId; return await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid).Select(x => x.Serial).FirstOrDefaultAsync(); } //////////////////////////////////////////////////////////////////////////////////////////////// //SEARCH // private async Task WorkOrderSearchIndexAsync(WorkOrder obj, bool isNew) { var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType); DigestSearchText(obj, SearchParams); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task GetSearchResultSummary(long id) { var obj = await ct.WorkOrder.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);//# NOTE intentionally not calling workorder get async here, don't need the whole graph var SearchParams = new Search.SearchIndexProcessObjectParameters(); DigestSearchText(obj, SearchParams); return SearchParams; } public void DigestSearchText(WorkOrder obj, Search.SearchIndexProcessObjectParameters searchParams) { if (obj != null) searchParams.AddText(obj.Notes).AddText(obj.Serial).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); } //////////////////////////////////////////////////////////////////////////////////////////////// // "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.WorkOrder.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 (WorkOrderItem wi in wo.Items) { foreach (WorkOrderItemExpense o in wi.Expenses) await ExpensePopulateVizFields(o, true); foreach (WorkOrderItemLabor o in wi.Labors) await LaborPopulateVizFields(o, true); foreach (WorkOrderItemLoan o in wi.Loans) await LoanPopulateVizFields(o, null, true); foreach (WorkOrderItemPart o in wi.Parts) await PartPopulateVizFields(o, true); foreach (WorkOrderItemTravel o in wi.Travels) await TravelPopulateVizFields(o, true); foreach (WorkOrderItemOutsideService o in wi.OutsideServices) await OutsideServicePopulateVizFields(o, true); } foreach (WorkOrderItem wi in wo.Items) { foreach (WorkOrderItemExpense o in wi.Expenses) GrandTotal += o.LineTotalViz; foreach (WorkOrderItemLabor o in wi.Labors) GrandTotal += o.LineTotalViz; foreach (WorkOrderItemLoan o in wi.Loans) GrandTotal += o.LineTotalViz; foreach (WorkOrderItemPart o in wi.Parts) GrandTotal += o.LineTotalViz; foreach (WorkOrderItemTravel o in wi.Travels) GrandTotal += o.LineTotalViz; foreach (WorkOrderItemOutsideService o in wi.OutsideServices) GrandTotal += o.LineTotalViz; } return GrandTotal; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // //Can save or update? private async Task WorkOrderValidateAsync(WorkOrder proposedObj, WorkOrder currentObj) { //This may become necessary for v8migrate, leaving out for now //skip validation if seeding //if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; //Check restricted role preventing create if (isNew && UserIsRestrictedType) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrder.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj); //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void WorkOrderValidateCanDelete(WorkOrder dbObject) { //Check restricted role preventing create if (UserIsRestrictedType) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET PARTIAL WORKORDER FOR REPORTING // (returns workorder consisting only of the path from child or grandchild up to header populated // with display data for reporting) // internal async Task WorkOrderGetPartialAsync(AyaType ayaType, long id, bool includeWoItemDescendants, bool populateForReporting) { //if it's the entire workorder just get, populate and return as normal if (ayaType == AyaType.WorkOrder) return await WorkOrderGetAsync(id, true, false, populateForReporting); var wid = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct); //get header only var ret = await ct.WorkOrder.AsNoTracking().SingleOrDefaultAsync(x => x.Id == wid.ParentId); //not found don't bomb, just return null if (ret == null) return ret; //explicit load subitems as required... WorkOrderItem woitem = null; //it's requesting a fully populated woitem so do that here if (includeWoItemDescendants) { woitem = await ct.WorkOrderItem.AsSplitQuery() .AsNoTracking() .Include(wi => wi.Expenses) .Include(wi => wi.Labors) .Include(wi => wi.Loans) .Include(wi => wi.Parts) .Include(wi => wi.PartRequests) .Include(wi => wi.ScheduledUsers) .Include(wi => wi.Tasks) .Include(wi => wi.Travels) .Include(wi => wi.Units) .Include(wi => wi.OutsideServices) .SingleOrDefaultAsync(z => z.Id == wid.ChildItemId); } else { //get the single workorder item required woitem = await ct.WorkOrderItem.AsNoTracking().SingleOrDefaultAsync(x => x.Id == wid.ChildItemId); switch (ayaType) { case AyaType.WorkOrderItemExpense: woitem.Expenses.Add(await ct.WorkOrderItemExpense.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemLabor: woitem.Labors.Add(await ct.WorkOrderItemLabor.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemLoan: woitem.Loans.Add(await ct.WorkOrderItemLoan.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemPart: woitem.Parts.Add(await ct.WorkOrderItemPart.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemPartRequest: woitem.PartRequests.Add(await ct.WorkOrderItemPartRequest.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemScheduledUser: woitem.ScheduledUsers.Add(await ct.WorkOrderItemScheduledUser.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemTask: woitem.Tasks.Add(await ct.WorkOrderItemTask.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemTravel: woitem.Travels.Add(await ct.WorkOrderItemTravel.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemOutsideService: woitem.OutsideServices.Add(await ct.WorkOrderItemOutsideService.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; case AyaType.WorkOrderItemUnit: woitem.Units.Add(await ct.WorkOrderItemUnit.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync()); break; } } if (woitem != null) ret.Items.Add(woitem); await WorkOrderPopulateVizFields(ret, false, populateForReporting); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //REPORTING // public async Task GetReportData(DataListSelectedRequest dataListSelectedRequest) { //workorder reports for entire workorder or just sub parts all go through here //if the ayatype is a descendant of the workorder then only the portion of the workorder from that descendant directly up to the header will be populated and returned //however if the report template has includeWoItemDescendants=true then the woitems is fully populated var idList = dataListSelectedRequest.SelectedRowIds; JArray ReportData = new JArray(); while (idList.Any()) { var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE); idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray(); List batchResults = new List(); foreach (long batchId in batch) batchResults.Add(await WorkOrderGetPartialAsync(dataListSelectedRequest.AType, batchId, dataListSelectedRequest.IncludeWoItemDescendants, true)); foreach (WorkOrder w in batchResults) { var jo = JObject.FromObject(w); //WorkOrder header custom fields if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"])) jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]); //WorkOrderItem custom fields foreach (JObject jItem in jo["Items"]) { if (!JsonUtil.JTokenIsNullOrEmpty(jItem["CustomFields"])) jItem["CustomFields"] = JObject.Parse((string)jItem["CustomFields"]); //WorkOrderItemUnit custom fields foreach (JObject jUnit in jItem["Units"]) { if (!JsonUtil.JTokenIsNullOrEmpty(jUnit["CustomFields"])) jUnit["CustomFields"] = JObject.Parse((string)jUnit["CustomFields"]); } } ReportData.Add(jo); } } return ReportData; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task WorkOrderPopulateVizFields(WorkOrder o, bool headerOnly, bool populateForReporting) { o.UserIsRestrictedType = UserIsRestrictedType; o.UserIsTechRestricted = UserIsTechRestricted; o.UserIsSubContractorFull = UserIsSubContractorFull; o.UserIsSubContractorRestricted = UserIsSubContractorRestricted; o.UserCanViewPartCosts = UserCanViewPartCosts; o.UserCanViewLaborOrTravelRateCosts = UserCanViewLaborOrTravelRateCosts; o.UserCanViewLoanerCosts = UserCanViewLoanerCosts; if (!headerOnly) { foreach (var v in o.States) await StatePopulateVizFields(v); foreach (var v in o.Items) await ItemPopulateVizFields(v, populateForReporting); } //popup Alert notes //Customer notes first then others below var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => new { AlertViz = x.PopUpNotes, x.TechNotes, CustomerViz = x.Name }).FirstOrDefaultAsync(); if (!string.IsNullOrWhiteSpace(custInfo.AlertViz)) { o.AlertViz = $"{await Translate("Customer")} - {await Translate("AlertNotes")}\n{custInfo.AlertViz}\n\n"; } if (!string.IsNullOrWhiteSpace(custInfo.TechNotes)) { o.CustomerTechNotesViz = $"{await Translate("CustomerTechNotes")}\n{custInfo.TechNotes}\n\n"; } o.CustomerViz = custInfo.CustomerViz; if (o.ProjectId != null) o.ProjectViz = await ct.Project.AsNoTracking().Where(x => x.Id == o.ProjectId).Select(x => x.Name).FirstOrDefaultAsync(); if (o.ContractId != null) { var contractVizFields = await ct.Contract.AsNoTracking().Where(x => x.Id == o.ContractId).Select(x => new { Name = x.Name, AlertNotes = x.AlertNotes }).FirstOrDefaultAsync(); o.ContractViz = contractVizFields.Name; if (!string.IsNullOrWhiteSpace(contractVizFields.AlertNotes)) { o.AlertViz += $"{await Translate("Contract")}\n{contractVizFields.AlertNotes}\n\n"; } } else o.ContractViz = "-"; if (o.FromQuoteId != null) o.FromQuoteViz = await ct.Quote.AsNoTracking().Where(x => x.Id == o.FromQuoteId).Select(x => x.Serial.ToString()).FirstOrDefaultAsync(); if (o.FromPMId != null) o.FromPMViz = await ct.PM.AsNoTracking().Where(x => x.Id == o.FromPMId).Select(x => x.Serial.ToString()).FirstOrDefaultAsync(); if (o.FromCSRId != null) o.FromCSRViz = await ct.CustomerServiceRequest.AsNoTracking().Where(x => x.Id == o.FromCSRId).Select(x => x.Name).FirstOrDefaultAsync(); } //////////////////////////////////////////////////////////////////////////////////////////////// // IMPORT EXPORT // public async Task GetExportData(DataListSelectedRequest dataListSelectedRequest) { //for now just re-use the report data code //this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time return await GetReportData(dataListSelectedRequest); } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // public async Task HandleJobAsync(OpsJob job) { switch (job.JobType) { case JobType.BatchCoreObjectOperation: await ProcessBatchJobAsync(job); break; default: throw new System.ArgumentOutOfRangeException($"WorkOrder.HandleJob-> Invalid job type{job.JobType.ToString()}"); } } private async Task ProcessBatchJobAsync(OpsJob job) { await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running); await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}"); List idList = new List(); long FailedObjectCount = 0; JObject jobData = JObject.Parse(job.JobInfo); if (jobData.ContainsKey("idList")) idList = ((JArray)jobData["idList"]).ToObject>(); else idList = await ct.Widget.Select(z => z.Id).ToListAsync(); bool SaveIt = false; foreach (long id in idList) { try { SaveIt = false; ClearErrors(); ICoreBizObjectModel o = null; //save a fetch if it's a delete if (job.SubType != JobSubType.Delete) o = await GetWorkOrderGraphItem(job.AType, id); switch (job.SubType) { case JobSubType.TagAddAny: case JobSubType.TagAdd: case JobSubType.TagRemoveAny: case JobSubType.TagRemove: case JobSubType.TagReplaceAny: case JobSubType.TagReplace: SaveIt = TagBiz.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType); break; case JobSubType.Delete: if (!await DeleteWorkOrderGraphItem(job.AType, id)) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}"); FailedObjectCount++; } break; default: throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}"); } if (SaveIt) { o = await PutWorkOrderGraphItem(job.AType, o); if (o == null) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}"); FailedObjectCount++; } } } catch (Exception ex) { await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})"); await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex)); } } await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}"); await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed); } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task WorkOrderHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrder oProposed = (WorkOrder)proposedObj; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);//Note: will properly handle all delete events and event removal if deleted //SPECIFIC EVENTS FOR THIS OBJECT WorkOrder oCurrent = null; bool SameTags = true; if (currentObj != null) { oCurrent = (WorkOrder)currentObj; SameTags = NotifyEventHelper.TwoObjectsHaveSameTags(proposedObj.Tags, currentObj.Tags); } #region COMPLETE BY OVERDUE if (ayaEvent == AyaEvent.Created && oProposed.CompleteByDate != null) { //WorkorderCompletedStatusOverdue Created here on workorder creation for any subscribers //State notify event processor below has more notes and details and will remove this event if set to a completed state //If new and has completeby then can do notification var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompletedStatusOverdue).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(proposedObj.Tags, sub.Tags)) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.WorkorderCompletedStatusOverdue, UserId = sub.UserId, AyaType = proposedObj.AyaType, ObjectId = proposedObj.Id, NotifySubscriptionId = sub.Id, Name = oProposed.Serial.ToString(), EventDate = (DateTime)oProposed.CompleteByDate }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//CREATED overdue completion if (ayaEvent == AyaEvent.Modified) {// WorkorderCompletedStatusOverdue modified in some way, could be tags, could be date either of which is relevant to this notification block //differences requiring re-processing of notification?? if (oProposed.CompleteByDate != oCurrent.CompleteByDate || !SameTags) { await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.WorkorderCompletedStatusOverdue); //new has date? if (oProposed.CompleteByDate != null) { var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompletedStatusOverdue).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(proposedObj.Tags, sub.Tags)) { NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.WorkorderCompletedStatusOverdue, UserId = sub.UserId, AyaType = proposedObj.AyaType, ObjectId = proposedObj.Id, NotifySubscriptionId = sub.Id, Name = oProposed.Serial.ToString(), EventDate = (DateTime)oProposed.CompleteByDate }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } } } }//CREATED overdue completion #endregion #region CustomerServiceImminent "Service Reminder" if (ayaEvent == AyaEvent.Created && oProposed.ServiceDate != null && oProposed.ServiceDate > DateTime.UtcNow) { var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.CustomerServiceImminent).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 workorder doesn't have a customer id then it's not going to match no matter what if (oProposed.CustomerId == 0) continue; var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId); //Are they allowed right now to use this type of notification? if (!customerUserRights.NotifyWOCreated) continue; //is this their related work order? if (UserInfo.CustomerId != oProposed.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 workorder 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 workorder doesn't qualify) var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == oProposed.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 continue;//only customers can subscribe to this particular notification //No tag match for this one NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.CustomerServiceImminent, UserId = sub.UserId, AyaType = proposedObj.AyaType, ObjectId = proposedObj.Id, NotifySubscriptionId = sub.Id, Name = oProposed.Serial.ToString(), EventDate = (DateTime)oProposed.ServiceDate }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } }//CustomerServiceImminent "Service Reminder" if (ayaEvent == AyaEvent.Modified) {// CustomerServiceImminent "Service Reminder" //differences requiring re-processing of notification if (oProposed.ServiceDate != oCurrent.ServiceDate) { await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.CustomerServiceImminent); //new has date? if (oProposed.ServiceDate != null && oProposed.ServiceDate > DateTime.UtcNow) { var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.CustomerServiceImminent).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 workorder doesn't have a customer id then it's not going to match no matter what if (oProposed.CustomerId == 0) continue; var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId); //Are they allowed right now to use this type of notification? if (!customerUserRights.NotifyWOCreated) continue; //is this their related work order? if (UserInfo.CustomerId != oProposed.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 workorder 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 workorder doesn't qualify) var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == oProposed.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 continue;//only customers can subscribe to this particular notification //No tag match for this one NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.CustomerServiceImminent, UserId = sub.UserId, AyaType = proposedObj.AyaType, ObjectId = proposedObj.Id, NotifySubscriptionId = sub.Id, Name = oProposed.Serial.ToString(), EventDate = (DateTime)oProposed.ServiceDate }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } } }//CustomerServiceImminent "Service Reminder" //# WorkorderCreatedForCustomer - Customer / headoffice notification if (oProposed.CustomerId != 0 && (ayaEvent == AyaEvent.Created || (ayaEvent == AyaEvent.Modified && oCurrent.CustomerId != oProposed.CustomerId))) { //look for potential subscribers var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCreatedForCustomer).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 workorder doesn't have a customer id then it's not going to match no matter what if (oProposed.CustomerId == 0) continue; var customerUserRights = await UserBiz.CustomerUserEffectiveRightsAsync(sub.UserId); //Are they allowed right now to use this type of notification? if (!customerUserRights.NotifyWOCreated) continue; //is this their related work order? if (UserInfo.CustomerId != oProposed.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 workorder 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 workorder doesn't qualify) var CustomerInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == oProposed.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 continue;//only customers can subscribe to this particular notification //Ok, we're here so it must be ok to notify user NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.WorkorderCreatedForCustomer, UserId = sub.UserId, AyaType = AyaType.WorkOrder, ObjectId = oProposed.Id, NotifySubscriptionId = sub.Id, Name = $"{oProposed.Serial.ToString()}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } }//WorkorderCreatedForCustomer #endregion }//end of process notifications #endregion workorder level /* ███████╗████████╗ █████╗ ████████╗███████╗███████╗ ██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██╔════╝██╔════╝ ███████╗ ██║ ███████║ ██║ █████╗ ███████╗ ╚════██║ ██║ ██╔══██║ ██║ ██╔══╝ ╚════██║ ███████║ ██║ ██║ ██║ ██║ ███████╗███████║ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝ */ #region WorkOrderState level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task StateExistsAsync(long id) { return await ct.WorkOrderState.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task StateCreateAsync(WorkOrderState newObject) { await StateValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.WorkOrderState.AddAsync(newObject); var wo = await ct.WorkOrder.FirstOrDefaultAsync(x => x.Id == newObject.WorkOrderId); var newStatusInfo = await ct.WorkOrderStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == newObject.WorkOrderStatusId); //Set duration to completed in workorder header //Clear it if set and not completed state //or //Set it if not set and completed state if (newStatusInfo.Completed && wo.DurationToCompleted == TimeSpan.Zero) wo.DurationToCompleted = DateTime.UtcNow - wo.CreatedDate; else if (wo.DurationToCompleted != TimeSpan.Zero && !newStatusInfo.Completed) wo.DurationToCompleted = TimeSpan.Zero; wo.LastStatusId = newObject.WorkOrderStatusId; await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderStatus, AyaEvent.Created), ct); await StateHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task StateGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.WorkOrderState.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderStatus, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task StatePopulateVizFields(WorkOrderState o) { o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // (note: this would only ever be called when a workorder is deleted, there is no direct delete) internal async Task StateDeleteAsync(long workOrderId, IDbContextTransaction parentTransaction) { var stateList = await ct.WorkOrderState.AsNoTracking().Where(z => z.WorkOrderId == workOrderId).ToListAsync(); foreach (var wostate in stateList) { ct.WorkOrderState.Remove(wostate); await ct.SaveChangesAsync(); //no need to call this because it's only going to run this method if the workorder is deleted and //via process standard notifciation events for workorder deletion will remove any state delayed notifications anyway so //nothing to call or do here related to notification // await StateHandlePotentialNotificationEvent(AyaEvent.Deleted, wostate); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task StateValidateAsync(WorkOrderState proposedObj, WorkOrderState currentObj) { //of all restricted users, only a restricted tech can change status if (UserIsSubContractorFull || UserIsSubContractorRestricted) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //run validation and biz rules bool isNew = currentObj == null; //does it have a valid workorder id if (proposedObj.WorkOrderId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId"); else if (!await WorkOrderExistsAsync(proposedObj.WorkOrderId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderId"); } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task StateHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderState oProposed = (WorkOrderState)proposedObj; var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == oProposed.WorkOrderId).Select(x => new { x.Serial, x.Tags, x.CustomerId }).FirstOrDefaultAsync(); WorkOrderStatus wos = await ct.WorkOrderStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.WorkOrderStatusId); //for notification purposes because has no name / tags field itself oProposed.Name = WorkorderInfo.Serial.ToString(); oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS //NONE: state notifications are specific and not the same as for general objects so don't process standard events //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 workorder 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 workorder 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 //## CREATED (this is the only possible notification CREATION ayaEvent type for a workorder 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.WorkOrderStatusId).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.WorkOrder, ObjectId = oProposed.WorkOrderId, NotifySubscriptionId = sub.Id, Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//workorder 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.WorkOrderStatusId).ToListAsync(); foreach (var sub in subs) { //not for inactive users if (!await UserBiz.UserIsActive(sub.UserId)) continue; //WorkOrder 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.WorkOrder, ObjectId = oProposed.WorkOrderId, NotifySubscriptionId = sub.Id, Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//workorder 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 //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 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.WorkOrderId, NotifyEventType.WorkorderCompletedStatusOverdue); } }//workorder complete by overdue change event //# WorkorderTotalExceedsThreshold / "The Andy" { if (wos.Completed) { //see if any subscribers to the workorder total exceeds notification //that are active then proceed to fetch billed woitem children and total workorder and send notification if necessary bool haveTotal = false; decimal GrandTotal = 0m; //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; //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; //get the total because we have at least one subscriber and matching tags if (haveTotal == false) { GrandTotal = await WorkorderGrandTotalAsync(oProposed.WorkOrderId, ct); haveTotal = true; //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.WorkOrder, oProposed.WorkOrderId, 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.WorkOrder, ObjectId = oProposed.WorkOrderId, 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 //# WorkorderCompleted - Customer AND User but customer only notifies if it's their workorder { 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 workorder 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 workorder 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 workorder 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.WorkOrder, ObjectId = oProposed.WorkOrderId, 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 #endregion work order STATE level /* ██╗████████╗███████╗███╗ ███╗███████╗ ██║╚══██╔══╝██╔════╝████╗ ████║██╔════╝ ██║ ██║ █████╗ ██╔████╔██║███████╗ ██║ ██║ ██╔══╝ ██║╚██╔╝██║╚════██║ ██║ ██║ ███████╗██║ ╚═╝ ██║███████║ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ */ #region WorkOrderItem level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ItemExistsAsync(long id) { return await ct.WorkOrderItem.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ItemCreateAsync(WorkOrderItem newObject) { await ItemValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItem.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItem, AyaEvent.Created), ct); await ItemSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await ItemPopulateVizFields(newObject, false); await ItemHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ItemGetAsync(long id, bool logTheGetEvent = true) { //Restricted users can not fetch a woitem directly //arbitrary decision so don't have to put in all the cleanup code //because from our own UI they wouldn't fetch this anyway and //so this is only to cover api use by 3rd parties if (UserIsRestrictedType) { return null; } //Note: there could be rules checking here in future, i.e. can only get own workorder or something //if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now //https://docs.microsoft.com/en-us/ef/core/querying/related-data //docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections var ret = await ct.WorkOrderItem.AsSplitQuery().AsNoTracking() .Include(wi => wi.Expenses) .Include(wi => wi.Labors) .Include(wi => wi.Loans) .Include(wi => wi.Parts) .Include(wi => wi.PartRequests) .Include(wi => wi.ScheduledUsers) .Include(wi => wi.Tasks) .Include(wi => wi.Travels) .Include(wi => wi.Units) .Include(wi => wi.OutsideServices) .SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItem, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ItemPutAsync(WorkOrderItem putObject) { //Note: this is intentionally not using the getasync because //doing so would also fetch the children which would then get deleted on save since putobject has no children var dbObject = await ct.WorkOrderItem.AsNoTracking().FirstOrDefaultAsync(z => z.Id == putObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } putObject.Tags = TagBiz.NormalizeTags(putObject.Tags); putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields); await ItemValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ItemExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, AyaType.WorkOrderItem, AyaEvent.Modified), ct); await ItemSearchIndexAsync(putObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); await ItemPopulateVizFields(putObject, false); await ItemHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ItemDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await ct.WorkOrderItem.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND); return false; } ItemValidateCanDelete(dbObject); if (HasErrors) return false; //collect the child id's to delete var ExpenseIds = await ct.WorkOrderItemExpense.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var LaborIds = await ct.WorkOrderItemLabor.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var LoanIds = await ct.WorkOrderItemLoan.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var PartIds = await ct.WorkOrderItemPart.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var PartRequestIds = await ct.WorkOrderItemPartRequest.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var ScheduledUserIds = await ct.WorkOrderItemScheduledUser.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var TaskIds = await ct.WorkOrderItemTask.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var TravelIds = await ct.WorkOrderItemTravel.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var UnitIds = await ct.WorkOrderItemUnit.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); var OutsideServiceIds = await ct.WorkOrderItemOutsideService.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync(); //Delete children foreach (long ItemId in ExpenseIds) if (!await ExpenseDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in LaborIds) if (!await LaborDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in LoanIds) if (!await LoanDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in PartIds) if (!await PartDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in PartRequestIds) if (!await PartRequestDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in ScheduledUserIds) if (!await ScheduledUserDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in TaskIds) if (!await TaskDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in TravelIds) if (!await TravelDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in UnitIds) if (!await UnitDeleteAsync(ItemId, transaction)) return false; foreach (long ItemId in OutsideServiceIds) if (!await OutsideServiceDeleteAsync(ItemId, transaction)) return false; ct.WorkOrderItem.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "wo:" + dbObject.WorkOrderId.ToString(), ct);//FIX wo?? Not sure what is best here; revisit await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct); //all good do the commit if it's ours if (parentTransaction == null) await transaction.CommitAsync(); await ItemHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } private async Task ItemSearchIndexAsync(WorkOrderItem obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItem); SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task ItemGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItem.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);//# Note, intentionally not calling ItemGetAsync here as don't want whole graph var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task ItemPopulateVizFields(WorkOrderItem o, bool populateForReporting) { foreach (var v in o.Expenses) await ExpensePopulateVizFields(v); foreach (var v in o.Labors) await LaborPopulateVizFields(v); foreach (var v in o.Loans) await LoanPopulateVizFields(v); foreach (var v in o.OutsideServices) await OutsideServicePopulateVizFields(v); foreach (var v in o.PartRequests) await PartRequestPopulateVizFields(v); foreach (var v in o.Parts) await PartPopulateVizFields(v); foreach (var v in o.ScheduledUsers) await ScheduledUserPopulateVizFields(v); foreach (var v in o.Tasks) await TaskPopulateVizFields(v); foreach (var v in o.Travels) await TravelPopulateVizFields(v); foreach (var v in o.Units) await UnitPopulateVizFields(v, populateForReporting); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ItemValidateAsync(WorkOrderItem proposedObj, WorkOrderItem currentObj) { //run validation and biz rules bool isNew = currentObj == null; //does it have a valid workorder id if (proposedObj.WorkOrderId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId"); else if (!await WorkOrderExistsAsync(proposedObj.WorkOrderId)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderId"); //Check restricted role preventing create if (isNew && UserIsRestrictedType) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (string.IsNullOrWhiteSpace(proposedObj.Notes)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Notes"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItem.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void ItemValidateCanDelete(WorkOrderItem obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //Check restricted role preventing create if (UserIsRestrictedType) { AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return;//this is a completely disqualifying error } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItem)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task ItemHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItem oProposed = (WorkOrderItem)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name field itself if (WorkorderInfo != null) oProposed.Name = WorkorderInfo.Serial.ToString(); else oProposed.Name = "??"; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item level /* ███████╗██╗ ██╗██████╗ ███████╗███╗ ██╗███████╗███████╗███████╗ ██╔════╝╚██╗██╔╝██╔══██╗██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝ █████╗ ╚███╔╝ ██████╔╝█████╗ ██╔██╗ ██║███████╗█████╗ ███████╗ ██╔══╝ ██╔██╗ ██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══╝ ╚════██║ ███████╗██╔╝ ██╗██║ ███████╗██║ ╚████║███████║███████╗███████║ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝ */ #region WorkOrderItemExpense level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExpenseExistsAsync(long id) { return await ct.WorkOrderItemExpense.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ExpenseCreateAsync(WorkOrderItemExpense newObject) { await ExpenseValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.WorkOrderItemExpense.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await ExpenseSearchIndexAsync(newObject, true); await ExpensePopulateVizFields(newObject); await ExpenseHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ExpenseGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorFull || UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.WorkOrderItemExpense.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (UserIsTechRestricted && ret.UserId != UserId)//tech restricted can only see their own expenses { AddError(ApiErrorCode.NOT_AUTHORIZED); return null; } if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ExpensePutAsync(WorkOrderItemExpense putObject) { var dbObject = await ExpenseGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await ExpenseValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ExpenseExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await ExpenseSearchIndexAsync(putObject, false); await ExpensePopulateVizFields(putObject); await ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ExpenseDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await ExpenseGetAsync(id, false); ExpenseValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemExpense.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await ExpenseHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task ExpenseSearchIndexAsync(WorkOrderItemExpense obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Name).AddText(obj.Description); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task ExpenseGetSearchResultSummary(long id) { var obj = await ExpenseGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Description).AddText(obj.Name); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task ExpensePopulateVizFields(WorkOrderItemExpense o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.UserId != null) o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); } TaxCode Tax = null; if (o.ChargeTaxCodeId != null) Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ChargeTaxCodeId); if (Tax != null) o.TaxCodeViz = Tax.Name; //Calculate totals and taxes if (o.ChargeToCustomer) { o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.ChargeAmount * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.ChargeAmount + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.ChargeAmount * (Tax.TaxBPct / 100)); } } o.LineTotalViz = o.ChargeAmount + o.TaxAViz + o.TaxBViz; } else { o.LineTotalViz = o.ChargeAmount + o.TaxPaid; } } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ExpenseValidateAsync(WorkOrderItemExpense proposedObj, WorkOrderItemExpense currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsSubContractorFull || UserIsSubContractorRestricted) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId))) { //no edits allowed on other people's records AddError(ApiErrorCode.NOT_AUTHORIZED); return; } if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (!isNew && UserIsTechRestricted) { //Existing record so just make sure they haven't changed the not changeable fields from the db version //Expenses: add (no user selection defaults to themselves), view, partial fields available // to edit or delete only where they are the selected user and only edit fields //Summary, Cost, Tax paid, Description //note that UI will prevent this, this rule is only backup for 3rd party api users if (currentObj.ChargeAmount != proposedObj.ChargeAmount) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeAmount"); //if (currentObj.TaxPaid != proposedObj.TaxPaid) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "TaxPaid"); if (currentObj.ChargeTaxCodeId != proposedObj.ChargeTaxCodeId) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeTaxCodeId"); if (currentObj.ReimburseUser != proposedObj.ReimburseUser) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReimburseUser"); if (currentObj.ChargeToCustomer != proposedObj.ChargeToCustomer) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeToCustomer"); } if (isNew && UserIsTechRestricted) { //NEW record, they are not allowed to set several fields so make sure they are still at their defaults /* from client new expense record: concurrency: 0, description: null, name: null, totalCost: 0, chargeAmount: 0, taxPaid: 0, chargeTaxCodeId: null, taxCodeViz: null, reimburseUser: false, userId: null, userViz: null, chargeToCustomer: false, isDirty: true, workOrderItemId: this.value.items[this.activeWoItemIndex].id, uid: Date.now() //used for */ if (proposedObj.ChargeAmount != 0) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeAmount"); // if (proposedObj.TaxPaid != 0) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "TaxPaid"); if (proposedObj.ChargeTaxCodeId != null) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeTaxCodeId"); if (proposedObj.ReimburseUser != false) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReimburseUser"); if (proposedObj.ChargeToCustomer != false) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeToCustomer"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemExpense.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields // CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void ExpenseValidateCanDelete(WorkOrderItemExpense obj) { if (UserIsSubContractorFull || UserIsSubContractorRestricted) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsTechRestricted && obj.UserId != UserId) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemExpense)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task ExpenseHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemExpense oProposed = (WorkOrderItemExpense)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item EXPENSE level /* ██╗ █████╗ ██████╗ ██████╗ ██████╗ ██║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗ ██║ ███████║██████╔╝██║ ██║██████╔╝ ██║ ██╔══██║██╔══██╗██║ ██║██╔══██╗ ███████╗██║ ██║██████╔╝╚██████╔╝██║ ██║ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝ */ #region WorkOrderItemLabor level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task LaborExistsAsync(long id) { return await ct.WorkOrderItemLabor.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task LaborCreateAsync(WorkOrderItemLabor newObject) { await LaborValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.WorkOrderItemLabor.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await LaborSearchIndexAsync(newObject, true); await LaborPopulateVizFields(newObject); await LaborHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task LaborGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.WorkOrderItemLabor.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (UserIsRestrictedType && ret.UserId != UserId) { AddError(ApiErrorCode.NOT_AUTHORIZED); return null; } if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task LaborPutAsync(WorkOrderItemLabor putObject) { var dbObject = await LaborGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await LaborValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await LaborExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await LaborSearchIndexAsync(putObject, false); await LaborPopulateVizFields(putObject); await LaborHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task LaborDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await LaborGetAsync(id, false); LaborValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemLabor.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await LaborHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task LaborSearchIndexAsync(WorkOrderItemLabor obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.ServiceDetails); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task LaborGetSearchResultSummary(long id) { var obj = await LaborGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.ServiceDetails); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task LaborPopulateVizFields(WorkOrderItemLabor o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.UserId != null) o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); } ServiceRate Rate = null; if (o.ServiceRateId != null) { Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.ServiceRateId); o.ServiceRateViz = Rate.Name; } TaxCode Tax = null; if (o.TaxCodeSaleId != null) Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId); if (Tax != null) o.TaxCodeViz = Tax.Name; o.PriceViz = 0; if (Rate != null) { o.CostViz = Rate.Cost; o.ListPriceViz = Rate.Charge; o.UnitOfMeasureViz = Rate.Unit; o.PriceViz = Rate.Charge;//default price used if not manual or contract override } //manual price overrides anything if (o.PriceOverride != null) o.PriceViz = (decimal)o.PriceOverride; else { //not manual so could potentially have a contract adjustment var c = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId); if (c != null) { decimal pct = 0; ContractOverrideType cot = ContractOverrideType.PriceDiscount; bool TaggedAdjustmentInEffect = false; //POTENTIAL CONTRACT ADJUSTMENTS //First check if there is a matching tagged service rate contract discount, that takes precedence if (c.ContractServiceRateOverrideItems.Count > 0) { //Iterate all contract tagged items in order of ones with the most tags first foreach (var csr in c.ContractServiceRateOverrideItems.OrderByDescending(z => z.Tags.Count)) if (csr.Tags.All(z => Rate.Tags.Any(x => x == z))) { if (csr.OverridePct != 0) { pct = csr.OverridePct / 100; cot = csr.OverrideType; TaggedAdjustmentInEffect = true; } } } //Generic discount? if (!TaggedAdjustmentInEffect && c.ServiceRatesOverridePct != 0) { pct = c.ServiceRatesOverridePct / 100; cot = c.ServiceRatesOverrideType; } //apply if discount found if (pct != 0) { if (cot == ContractOverrideType.CostMarkup) o.PriceViz = MoneyUtil.Round(o.CostViz + (o.CostViz * pct)); else if (cot == ContractOverrideType.PriceDiscount) o.PriceViz = MoneyUtil.Round(o.ListPriceViz - (o.ListPriceViz * pct)); } } } //Calculate totals and taxes //NET o.NetViz = MoneyUtil.Round(o.PriceViz * o.ServiceRateQuantity); //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; //RESTRICTIONS ON COST VISIBILITY? if (!UserCanViewLaborOrTravelRateCosts) { o.CostViz = 0; } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task LaborValidateAsync(WorkOrderItemLabor proposedObj, WorkOrderItemLabor currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId))) { //no edits allowed on other people's records AddError(ApiErrorCode.NOT_AUTHORIZED); return; } //Start date AND end date must both be null or both contain values if (proposedObj.ServiceStartDate == null && proposedObj.ServiceStopDate != null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServiceStartDate"); if (proposedObj.ServiceStartDate != null && proposedObj.ServiceStopDate == null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServiceStopDate"); //Start date before end date if (proposedObj.ServiceStartDate != null && proposedObj.ServiceStopDate != null) if (proposedObj.ServiceStartDate > proposedObj.ServiceStopDate) AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "ServiceStartDate"); if (proposedObj.ServiceRateQuantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ServiceRateQuantity"); if (proposedObj.NoChargeQuantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "NoChargeQuantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemLabor.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void LaborValidateCanDelete(WorkOrderItemLabor obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsRestrictedType) { //Labors: add (no user selection defaults to themselves), remove, view and edit only when they are the selected User if (obj.UserId != UserId) AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemLabor)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task LaborHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemLabor oProposed = (WorkOrderItemLabor)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name or tags field itself oProposed.Name = WorkorderInfo.Serial.ToString(); oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item LABOR level /* ██╗ ██████╗ █████╗ ███╗ ██╗ ██║ ██╔═══██╗██╔══██╗████╗ ██║ ██║ ██║ ██║███████║██╔██╗ ██║ ██║ ██║ ██║██╔══██║██║╚██╗██║ ███████╗╚██████╔╝██║ ██║██║ ╚████║ */ #region WorkOrderItemLoan level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task LoanExistsAsync(long id) { return await ct.WorkOrderItemLoan.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task LoanCreateAsync(WorkOrderItemLoan newObject) { await LoanValidateAsync(newObject, null); if (HasErrors) return null; else { await LoanBizActionsAsync(AyaEvent.Created, newObject, null); await ct.WorkOrderItemLoan.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await LoanSearchIndexAsync(newObject, true); await LoanPopulateVizFields(newObject); await LoanHandlePotentialNotificationEvent(AyaEvent.Created, newObject); if (newObject.ReturnDate == null) { var u = await ct.LoanUnit.FirstOrDefaultAsync(x => x.Id == newObject.LoanUnitId); if (u != null) { u.WorkOrderItemLoanId = newObject.Id; await ct.SaveChangesAsync(); } } return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task LoanGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.WorkOrderItemLoan.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task LoanPutAsync(WorkOrderItemLoan putObject) { var dbObject = await LoanGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await LoanValidateAsync(putObject, dbObject); if (HasErrors) return null; await LoanBizActionsAsync(AyaEvent.Modified, putObject, dbObject); bool changeOfUnit = dbObject.LoanUnitId != putObject.LoanUnitId; bool changeOfReturned = dbObject.ReturnDate != putObject.ReturnDate; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await LoanExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await LoanSearchIndexAsync(putObject, false); await LoanPopulateVizFields(putObject); await LoanHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); //update loan unitif (newObject.ReturnDate == null) //Returned or unit changed if (changeOfUnit || changeOfReturned) { var u = await ct.LoanUnit.FirstOrDefaultAsync(x => x.Id == dbObject.LoanUnitId); if (u != null && (u.WorkOrderItemLoanId == null || u.WorkOrderItemLoanId == putObject.Id)) { if (putObject.ReturnDate != null) { u.WorkOrderItemLoanId = null; } else { u.WorkOrderItemLoanId = putObject.Id; } await ct.SaveChangesAsync(); } } return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task LoanDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await LoanGetAsync(id, false); LoanValidateCanDelete(dbObject); if (HasErrors) return false; //note:deliberately doing collection here in case of weird issues var ul = await ct.LoanUnit.Where(x => x.WorkOrderItemLoanId == dbObject.Id).ToListAsync(); foreach (var u in ul) { if (u.WorkOrderItemLoanId == dbObject.Id)//only do this if it's flagged on *this* workorderitemloan u.WorkOrderItemLoanId = null; } await ct.SaveChangesAsync(); ct.WorkOrderItemLoan.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await LoanHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task LoanSearchIndexAsync(WorkOrderItemLoan obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Notes); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task LoanGetSearchResultSummary(long id) { var obj = await LoanGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task LoanPopulateVizFields(WorkOrderItemLoan o, List loanUnitRateEnumList = null, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (loanUnitRateEnumList == null) loanUnitRateEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList( StringUtil.TrimTypeName(typeof(LoanUnitRateUnit).ToString()), UserTranslationId, CurrentUserRoles); o.UnitOfMeasureViz = loanUnitRateEnumList.Where(x => x.Id == (long)o.Rate).Select(x => x.Name).First(); } LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.LoanUnitId); o.LoanUnitViz = loanUnit.Name; TaxCode Tax = null; if (o.TaxCodeId != null) Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId); if (Tax != null) o.TaxCodeViz = Tax.Name; //manual price overrides anything o.PriceViz = o.ListPrice; if (o.PriceOverride != null) o.PriceViz = (decimal)o.PriceOverride; //Currently not contract discounted so no further calcs need apply to priceViz //Calculate totals and taxes //NET o.NetViz = MoneyUtil.Round(o.PriceViz * o.Quantity); //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; //RESTRICTED COST FIELD?? if (!UserCanViewLoanerCosts) o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire } //////////////////////////////////////////////////////////////////////////////////////////////// //BIZ ACTIONS // // private async Task LoanBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemLoan newObj, WorkOrderItemLoan oldObj) { //automatic actions on record change, called AFTER validation //currently no processing required except for created or modified at this time if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified) return; //SNAPSHOT PRICING bool SnapshotPricing = true; //if modifed, see what has changed and should be re-applied if (ayaEvent == AyaEvent.Modified) { //If it wasn't a complete part change there is no need to set pricing if (newObj.LoanUnitId == oldObj.LoanUnitId && newObj.Rate == oldObj.Rate) { SnapshotPricing = false; //maintain old cost as it can come from the client as zero when it shouldn't be or someone using the api and setting it directly //but we will only allow the price *we* set at the server initially newObj.Cost = oldObj.Cost; } } //Pricing if (SnapshotPricing) { //default in case nothing to apply newObj.Cost = 0; newObj.ListPrice = 0; LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == newObj.LoanUnitId); if (loanUnit != null) { switch (newObj.Rate) { case LoanUnitRateUnit.None: break; case LoanUnitRateUnit.Hours: newObj.Cost = loanUnit.RateHourCost; newObj.ListPrice = loanUnit.RateHour; break; case LoanUnitRateUnit.HalfDays: newObj.Cost = loanUnit.RateHalfDayCost; newObj.ListPrice = loanUnit.RateHalfDay; break; case LoanUnitRateUnit.Days: newObj.Cost = loanUnit.RateDayCost; newObj.ListPrice = loanUnit.RateDay; break; case LoanUnitRateUnit.Weeks: newObj.Cost = loanUnit.RateWeekCost; newObj.ListPrice = loanUnit.RateWeek; break; case LoanUnitRateUnit.Months: newObj.Cost = loanUnit.RateMonthCost; newObj.ListPrice = loanUnit.RateMonth; break; case LoanUnitRateUnit.Years: newObj.Cost = loanUnit.RateYearCost; newObj.ListPrice = loanUnit.RateYear; break; } } } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task LoanValidateAsync(WorkOrderItemLoan proposedObj, WorkOrderItemLoan currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (proposedObj.LoanUnitId < 1 || !await ct.LoanUnit.AnyAsync(x => x.Id == proposedObj.LoanUnitId)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "LoanUnitId"); if (proposedObj.Quantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemLoan.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void LoanValidateCanDelete(WorkOrderItemLoan obj) { if (UserIsRestrictedType) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemLoan)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task LoanHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemLoan oProposed = (WorkOrderItemLoan)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name / tags field itself oProposed.Name = WorkorderInfo.Serial.ToString(); oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item LOAN level /* ██████╗ ██╗ ██╗████████╗███████╗██╗██████╗ ███████╗ ███████╗███████╗██████╗ ██╗ ██╗██╗ ██████╗███████╗ ██╔═══██╗██║ ██║╚══██╔══╝██╔════╝██║██╔══██╗██╔════╝ ██╔════╝██╔════╝██╔══██╗██║ ██║██║██╔════╝██╔════╝ ██║ ██║██║ ██║ ██║ ███████╗██║██║ ██║█████╗ ███████╗█████╗ ██████╔╝██║ ██║██║██║ █████╗ ██║ ██║██║ ██║ ██║ ╚════██║██║██║ ██║██╔══╝ ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██║██║ ██╔══╝ ╚██████╔╝╚██████╔╝ ██║ ███████║██║██████╔╝███████╗ ███████║███████╗██║ ██║ ╚████╔╝ ██║╚██████╗███████╗ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝╚══════╝ */ #region WorkOrderItemOutsideService level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task OutsideServiceExistsAsync(long id) { return await ct.WorkOrderItemOutsideService.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task OutsideServiceCreateAsync(WorkOrderItemOutsideService newObject) { await OutsideServiceValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.WorkOrderItemOutsideService.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await OutsideServiceSearchIndexAsync(newObject, true); await OutsideServicePopulateVizFields(newObject); await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task OutsideServiceGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted || UserIsSubContractorFull) //no access allowed at all return null; var ret = await ct.WorkOrderItemOutsideService.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemOutsideService, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task OutsideServicePutAsync(WorkOrderItemOutsideService putObject) { WorkOrderItemOutsideService dbObject = await OutsideServiceGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await OutsideServiceValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await OutsideServiceExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await OutsideServiceSearchIndexAsync(putObject, false); await OutsideServicePopulateVizFields(putObject); await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task OutsideServiceDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await OutsideServiceGetAsync(id, false); OutsideServiceValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemOutsideService.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task OutsideServiceSearchIndexAsync(WorkOrderItemOutsideService obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Notes).AddText(obj.RMANumber).AddText(obj.TrackingNumber); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task OutsideServiceGetSearchResultSummary(long id) { var obj = await OutsideServiceGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.RMANumber).AddText(obj.TrackingNumber); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task OutsideServicePopulateVizFields(WorkOrderItemOutsideService o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.UnitId != 0) o.UnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync(); if (o.VendorSentToId != null) o.VendorSentToViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentToId).Select(x => x.Name).FirstOrDefaultAsync(); if (o.VendorSentViaId != null) o.VendorSentViaViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentViaId).Select(x => x.Name).FirstOrDefaultAsync(); } TaxCode Tax = null; if (o.TaxCodeId != null) Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId); if (Tax != null) o.TaxCodeViz = Tax.Name; o.CostViz = o.ShippingCost + o.RepairCost; o.PriceViz = o.ShippingPrice + o.RepairPrice; //Currently not contract discounted so no further calcs need apply to priceViz //Calculate totals and taxes //NET o.NetViz = o.PriceViz;//just for standardization, no quantity so is redundant but reporting easier if all the same //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task OutsideServiceValidateAsync(WorkOrderItemOutsideService proposedObj, WorkOrderItemOutsideService currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (proposedObj.UnitId < 1 || !await ct.Unit.AnyAsync(x => x.Id == proposedObj.UnitId)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemOutsideService.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void OutsideServiceValidateCanDelete(WorkOrderItemOutsideService obj) { if (UserIsRestrictedType) { //no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemOutsideService)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task OutsideServiceHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { /* OutsideServiceOverdue = 16,//* Workorder object , WorkOrderItemOutsideService created / updated, sets advance notice on due date tag filterable OutsideServiceReceived = 17,//* Workorder object , WorkOrderItemOutsideService updated, instant notification when item received, tag filterable */ ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemOutsideService oProposed = (WorkOrderItemOutsideService)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); //for notification purposes because has no name / tags field itself oProposed.Name = WorkorderInfo.Serial.ToString(); oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT //## DELETED EVENTS //standard process above will remove any hanging around when deleted, nothing else specific here to deal with //## CREATED if (ayaEvent == AyaEvent.Created) { //OutsideServiceOverdue if (oProposed.ETADate != null) { //Conditions: tags + time delayed eta value var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceOverdue).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.OutsideServiceOverdue, UserId = sub.UserId, AyaType = AyaType.WorkOrderItemOutsideService, ObjectId = oProposed.Id, NotifySubscriptionId = sub.Id, EventDate = (DateTime)oProposed.ETADate, Name = $"{WorkorderInfo.Serial.ToString()}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//OutsideServiceOverdue //OutsideServiceReceived (here because it's possible a outside service is entered new with both an eta and received date if entered after the fact) if (oProposed.ReturnDate != null) { //Clear overdue ones as it's now received await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue); //Conditions: tags var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceReceived).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.OutsideServiceReceived, UserId = sub.UserId, AyaType = AyaType.WorkOrderItemOutsideService, ObjectId = oProposed.Id, NotifySubscriptionId = sub.Id, Name = $"{WorkorderInfo.Serial.ToString()}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//OutsideServiceReceived } //## MODIFIED if (ayaEvent == AyaEvent.Modified) { WorkOrderItemOutsideService oCurrent = (WorkOrderItemOutsideService)currentObj; //OutsideServiceOverdue //if modified then remove any potential prior ones in case irrelevant if (oProposed.ETADate != oCurrent.ETADate) { //eta changed, so first of all remove any prior ones await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue); //now can go ahead and add back again as appropriate if (oProposed.ETADate != null) { //Conditions: tags + time delayed eta value var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceOverdue).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.OutsideServiceOverdue, UserId = sub.UserId, AyaType = AyaType.WorkOrderItemOutsideService, ObjectId = oProposed.Id, NotifySubscriptionId = sub.Id, EventDate = (DateTime)oProposed.ETADate, Name = $"{WorkorderInfo.Serial.ToString()}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//OutsideServiceOverdue } //OutsideServiceReceived if (oProposed.ReturnDate != oCurrent.ReturnDate && oProposed.ReturnDate != null)//note that this is an instant notification type so no need to clear older ones like above which is time delayed { //Clear overdue ones as it's now received await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue); //Conditions: tags var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceReceived).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.OutsideServiceReceived, UserId = sub.UserId, AyaType = AyaType.WorkOrderItemOutsideService, ObjectId = oProposed.Id, NotifySubscriptionId = sub.Id, Name = $"{WorkorderInfo.Serial.ToString()}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//OutsideServiceReceived } }//end of process notifications #endregion work order item OUTSIDE SERVICE level /* ██████╗ █████╗ ██████╗ ████████╗███████╗ ██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝ ██████╔╝███████║██████╔╝ ██║ ███████╗ ██╔═══╝ ██╔══██║██╔══██╗ ██║ ╚════██║ ██║ ██║ ██║██║ ██║ ██║ ███████║ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ */ #region WorkOrderItemPart level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task PartExistsAsync(long id) { return await ct.WorkOrderItemPart.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task PartCreateAsync(WorkOrderItemPart newObject) { using (var transaction = await ct.Database.BeginTransactionAsync()) { await PartValidateAsync(newObject, null); if (HasErrors) return null; else { await PartBizActionsAsync(AyaEvent.Created, newObject, null); await ct.WorkOrderItemPart.AddAsync(newObject); await ct.SaveChangesAsync(); await PartInventoryAdjustmentAsync(AyaEvent.Created, newObject, null, transaction); if (HasErrors) { await transaction.RollbackAsync(); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await PartSearchIndexAsync(newObject, true); await transaction.CommitAsync(); await PartPopulateVizFields(newObject); await PartHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task PartGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.WorkOrderItemPart.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PartPutAsync(WorkOrderItemPart putObject) { using (var transaction = await ct.Database.BeginTransactionAsync()) { WorkOrderItemPart dbObject = await PartGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await PartValidateAsync(putObject, dbObject); if (HasErrors) return null; await PartBizActionsAsync(AyaEvent.Modified, putObject, dbObject); ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); await PartInventoryAdjustmentAsync(AyaEvent.Modified, putObject, dbObject, transaction); if (HasErrors) { await transaction.RollbackAsync(); return null; } } catch (DbUpdateConcurrencyException) { if (!await PartExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await PartSearchIndexAsync(putObject, false); await transaction.CommitAsync(); await PartPopulateVizFields(putObject); await PartHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task PartDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await PartGetAsync(id, false); PartValidateCanDelete(dbObject); if (HasErrors) return false; await PartBizActionsAsync(AyaEvent.Deleted, null, dbObject); ct.WorkOrderItemPart.Remove(dbObject); await ct.SaveChangesAsync(); await PartInventoryAdjustmentAsync(AyaEvent.Deleted, null, dbObject, transaction); if (HasErrors) { await transaction.RollbackAsync(); return false; } //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await PartHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task PartSearchIndexAsync(WorkOrderItemPart obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Description).AddText(obj.Serials); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task PartGetSearchResultSummary(long id) { var obj = await PartGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Description).AddText(obj.Serials); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task PartPopulateVizFields(WorkOrderItemPart o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.PartWarehouseId != 0) o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(); } Part part = null; if (o.PartId != 0) part = await ct.Part.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.PartId); else return;//this should never happen but this is insurance in case it does o.PartViz = part.PartNumber; o.PartNameViz = part.Name; o.UpcViz = part.UPC; TaxCode Tax = null; if (o.TaxPartSaleId != null) Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxPartSaleId); if (Tax != null) o.TaxCodeViz = Tax.Name; o.PriceViz = 0; if (part != null) { //COST & PRICE NOT SET HERE, SET IN BIZACTIONS SNAPSHOTTED // o.CostViz = part.Cost; // o.ListPriceViz = part.Retail; o.UnitOfMeasureViz = part.UnitOfMeasure; o.PriceViz = o.ListPrice;//default price used if not manual or contract override } //manual price overrides anything if (o.PriceOverride != null) o.PriceViz = (decimal)o.PriceOverride; else { //not manual so could potentially have a contract adjustment var c = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId); if (c != null) { decimal pct = 0; ContractOverrideType cot = ContractOverrideType.PriceDiscount; bool TaggedAdjustmentInEffect = false; //POTENTIAL CONTRACT ADJUSTMENTS //First check if there is a matching tagged contract discount, that takes precedence if (c.ContractPartOverrideItems.Count > 0) { //Iterate all contract tagged items in order of ones with the most tags first foreach (var cp in c.ContractPartOverrideItems.OrderByDescending(z => z.Tags.Count)) if (cp.Tags.All(z => part.Tags.Any(x => x == z))) { if (cp.OverridePct != 0) { pct = cp.OverridePct / 100; cot = cp.OverrideType; TaggedAdjustmentInEffect = true; } } } //Generic discount? if (!TaggedAdjustmentInEffect && c.ServiceRatesOverridePct != 0) { pct = c.ServiceRatesOverridePct / 100; cot = c.ServiceRatesOverrideType; } //apply if discount found if (pct != 0) { if (cot == ContractOverrideType.CostMarkup) o.PriceViz = MoneyUtil.Round(o.Cost + (o.Cost * pct)); else if (cot == ContractOverrideType.PriceDiscount) o.PriceViz = MoneyUtil.Round(o.ListPrice - (o.ListPrice * pct)); } } } //Calculate totals and taxes //NET o.NetViz = MoneyUtil.Round(o.PriceViz * o.Quantity); //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; //RESTRICTED COST FIELD?? if (!UserCanViewPartCosts) o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire } //////////////////////////////////////////////////////////////////////////////////////////////// //BIZ ACTIONS // // private async Task PartBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart oldObj) { //SNAPSHOT PRICING IF NECESSARY if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified) return; //SNAPSHOT PRICING bool SnapshotPricing = true; //if modifed, see what has changed and should be re-applied if (ayaEvent == AyaEvent.Modified) { //If it wasn't a complete part change there is no need to set pricing if (newObj.PartId == oldObj.PartId) { SnapshotPricing = false; //maintain old cost as it can come from the client as zero when it shouldn't be or someone using the api and setting it directly //but we will only allow the price *we* set at the server initially newObj.Cost = oldObj.Cost; } } //Pricing if (SnapshotPricing) { //default in case nothing to apply newObj.Cost = 0; newObj.ListPrice = 0; var s = await ct.Part.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.PartId); if (s != null) { newObj.Cost = s.Cost; newObj.ListPrice = s.Retail; } } } //////////////////////////////////////////////////////////////////////////////////////////////// //BIZ ACTIONS // // private async Task PartInventoryAdjustmentAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart oldObj, IDbContextTransaction transaction) { if (AyaNova.Util.ServerGlobalBizSettings.Cache.UseInventory) { PartInventoryBiz pib = new PartInventoryBiz(ct, UserId, UserTranslationId, CurrentUserRoles); //DELETED, HANDLE INVENTORY / RETURN SERIALS if (ayaEvent == AyaEvent.Deleted && oldObj.Quantity != 0) { dtInternalPartInventory pi = new dtInternalPartInventory { PartId = oldObj.PartId, PartWarehouseId = oldObj.PartWarehouseId, Quantity = oldObj.Quantity, SourceType = null,//null because the po no longer exists so this is technically a manual adjustment SourceId = null,//'' Description = await Translate("WorkOrderItemPart") + $" {oldObj.Serials} " + await Translate("EventDeleted") }; if (await pib.CreateAsync(pi) == null) { AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}"); return; } else { //return serial numbers to part if (!string.IsNullOrWhiteSpace(oldObj.Serials)) await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId); } } //CREATED, HANDLE INVENTORY / CONSUME SERIALS if (ayaEvent == AyaEvent.Created && newObj.Quantity != 0)//allow zero quantity parts on workorder as placeholder, serials will not be consumed { dtInternalPartInventory pi = new dtInternalPartInventory { PartId = newObj.PartId, PartWarehouseId = newObj.PartWarehouseId, Quantity = newObj.Quantity * -1, SourceType = AyaType.WorkOrderItemPart, SourceId = newObj.Id, Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated") }; if (await pib.CreateAsync(pi) == null) { if (pib.HasErrors) { foreach (var e in pib.Errors) { if (e.Code == ApiErrorCode.INSUFFICIENT_INVENTORY) AddError(e.Code, "Quantity", e.Message); else AddError(e.Code, e.Target, e.Message); } } //AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}"); return; } else { //Consume serial numbers from part if (!string.IsNullOrWhiteSpace(newObj.Serials)) await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId); } } //UPDATED, HANDLE INVENTORY / UPDATE SERIALS AS REQUIRED if (ayaEvent == AyaEvent.Modified) { //INVENTORY if (newObj.PartId != oldObj.PartId || newObj.Quantity != oldObj.Quantity) { //OUT with the old if (oldObj.Quantity != 0)//zero quantity doesn't affect inventory or serials { dtInternalPartInventory piOld = new dtInternalPartInventory { PartId = oldObj.PartId, PartWarehouseId = oldObj.PartWarehouseId, Quantity = oldObj.Quantity, SourceType = null,//null because the po no longer exists so this is technically a manual adjustment SourceId = null,//'' Description = await Translate("WorkOrderItemPart") + $" {oldObj.Serials} " + await Translate("EventDeleted") }; if (await pib.CreateAsync(piOld) == null) { AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({piOld.Description}):{pib.GetErrorsAsString()}"); return; } else { //return serial numbers to part if (!string.IsNullOrWhiteSpace(oldObj.Serials)) await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId); } } //IN with the new if (newObj.Quantity != 0) {//NOTE: zero quantity is considered to be a placeholder and no serials will be consumed, nor inventory affected dtInternalPartInventory piNew = new dtInternalPartInventory { PartId = newObj.PartId, PartWarehouseId = newObj.PartWarehouseId, Quantity = newObj.Quantity * -1, SourceType = AyaType.WorkOrderItemPart, SourceId = newObj.Id, Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated") }; if (await pib.CreateAsync(piNew) == null) { AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({piNew.Description}):{pib.GetErrorsAsString()}"); return; } else { //Consume serial numbers from part if (!string.IsNullOrWhiteSpace(newObj.Serials)) await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId); } } } //SERIALS else if (newObj.Serials != oldObj.Serials) { //NOTE: zero quantity is considered to be a placeholder and no serials will be consumed (hence not returned either) //return serial numbers to part if (oldObj.Quantity != 0 && !string.IsNullOrWhiteSpace(oldObj.Serials)) await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId); //Consume serial numbers from part if (newObj.Quantity != 0 && !string.IsNullOrWhiteSpace(newObj.Serials)) await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId); } } } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task PartValidateAsync(WorkOrderItemPart proposedObj, WorkOrderItemPart currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //Parts: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.Part, proposedObj.PartId, ct)) { AddError(ApiErrorCode.NOT_FOUND, "PartId"); return; } if (proposedObj.Quantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemPart.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void PartValidateCanDelete(WorkOrderItemPart obj) { if (UserIsRestrictedType) { //Parts: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemPart)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task PartHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemPart oProposed = (WorkOrderItemPart)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name / tags field itself oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item PARTS level /* ██████╗ █████╗ ██████╗ ████████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗███████╗███████╗████████╗███████╗ ██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝ ██╔══██╗██╔════╝██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝ ██████╔╝███████║██████╔╝ ██║█████╗██████╔╝█████╗ ██║ ██║██║ ██║█████╗ ███████╗ ██║ ███████╗ ██╔═══╝ ██╔══██║██╔══██╗ ██║╚════╝██╔══██╗██╔══╝ ██║▄▄ ██║██║ ██║██╔══╝ ╚════██║ ██║ ╚════██║ ██║ ██║ ██║██║ ██║ ██║ ██║ ██║███████╗╚██████╔╝╚██████╔╝███████╗███████║ ██║ ███████║ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝ */ #region WorkOrderItemPartRequest level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task PartRequestExistsAsync(long id) { return await ct.WorkOrderItemPartRequest.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task PartRequestCreateAsync(WorkOrderItemPartRequest newObject) { await PartRequestValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.WorkOrderItemPartRequest.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await PartRequestPopulateVizFields(newObject); await PartRequestHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task PartRequestGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.WorkOrderItemPartRequest.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PartRequestPutAsync(WorkOrderItemPartRequest putObject) { WorkOrderItemPartRequest dbObject = await PartRequestGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await PartRequestValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await PartRequestExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await PartRequestPopulateVizFields(putObject); await PartRequestHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task PartRequestDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await PartRequestGetAsync(id, false); PartRequestValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemPartRequest.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await PartRequestHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task PartRequestPopulateVizFields(WorkOrderItemPartRequest o) { if (o.PartWarehouseId != 0) o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(); Part part = null; if (o.PartId != 0) part = await ct.Part.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.PartId); o.PartViz = part.PartNumber; o.UpcViz = part.UPC; PurchaseOrder po = null; if (o.PurchaseOrderItemId != null) { var poid = await ct.PurchaseOrderItem.AsNoTracking().Where(x => x.Id == o.PurchaseOrderItemId).Select(x => x.PurchaseOrderId).FirstOrDefaultAsync(); if (poid != 0) po = await ct.PurchaseOrder.AsNoTracking().Where(x => x.Id == poid).FirstOrDefaultAsync(); } if (po != null) { o.PurchaseOrderViz = po.Serial.ToString(); o.PurchaseOrderIdViz = po.Id; o.PurchaseOrderDateViz = po.OrderedDate; o.PurchaseOrderExpectedDateViz = po.ExpectedReceiveDate; } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task PartRequestValidateAsync(WorkOrderItemPartRequest proposedObj, WorkOrderItemPartRequest currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //PartRequests: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.Part, proposedObj.PartId, ct)) { AddError(ApiErrorCode.NOT_FOUND, "PartId"); return; } if (proposedObj.Quantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemPartRequest.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void PartRequestValidateCanDelete(WorkOrderItemPartRequest obj) { if (UserIsRestrictedType) { //PartRequests: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemPartRequest)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task PartRequestHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemPartRequest oProposed = (WorkOrderItemPartRequest)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); proposedObj.Tags = WorkorderInfo.Tags; proposedObj.Name = WorkorderInfo.Serial.ToString(); //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item PART REQUEST level /* ███████╗ ██████╗██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ███████╗██████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗ ██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔════╝██╔══██╗ ██║ ██║██╔════╝██╔════╝██╔══██╗██╔════╝ ███████╗██║ ███████║█████╗ ██║ ██║██║ ██║██║ █████╗ ██║ ██║█████╗██║ ██║███████╗█████╗ ██████╔╝███████╗ ╚════██║██║ ██╔══██║██╔══╝ ██║ ██║██║ ██║██║ ██╔══╝ ██║ ██║╚════╝██║ ██║╚════██║██╔══╝ ██╔══██╗╚════██║ ███████║╚██████╗██║ ██║███████╗██████╔╝╚██████╔╝███████╗███████╗██████╔╝ ╚██████╔╝███████║███████╗██║ ██║███████║ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝ */ #region WorkOrderItemScheduledUser level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ScheduledUserExistsAsync(long id) { return await ct.WorkOrderItemScheduledUser.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task ScheduledUserCreateAsync(WorkOrderItemScheduledUser newObject) { await ScheduledUserValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.WorkOrderItemScheduledUser.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await ScheduledUserPopulateVizFields(newObject); await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task ScheduledUserGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.WorkOrderItemScheduledUser.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (UserIsRestrictedType && ret.UserId != UserId)//restricted users can only see their own return null; if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task ScheduledUserPutAsync(WorkOrderItemScheduledUser putObject) { WorkOrderItemScheduledUser dbObject = await ScheduledUserGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await ScheduledUserValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ScheduledUserExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await ScheduledUserPopulateVizFields(putObject); await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ScheduledUserDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await ScheduledUserGetAsync(id, false); ScheduledUserValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemScheduledUser.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task ScheduledUserPopulateVizFields(WorkOrderItemScheduledUser o) { if (o.UserId != null) o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); if (o.ServiceRateId != null) o.ServiceRateViz = await ct.ServiceRate.AsNoTracking().Where(x => x.Id == o.ServiceRateId).Select(x => x.Name).FirstOrDefaultAsync(); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ScheduledUserValidateAsync(WorkOrderItemScheduledUser proposedObj, WorkOrderItemScheduledUser currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } if (UserIsRestrictedType) { //Scheduled Users: view only where they are the selected User and convert to labor record AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (proposedObj.EstimatedQuantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "EstimatedQuantity"); //Start date AND end date must both be null or both contain values if (proposedObj.StartDate == null && proposedObj.StopDate != null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "StopDate"); if (proposedObj.StartDate != null && proposedObj.StopDate == null) AddError(ApiErrorCode.VALIDATION_REQUIRED, "StopDate"); //Start date before end date if (proposedObj.StartDate != null && proposedObj.StopDate != null) if (proposedObj.StartDate > proposedObj.StopDate) AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "StartDate"); //Scheduling conflict? if (!AyaNova.Util.ServerGlobalBizSettings.Cache.AllowScheduleConflicts && proposedObj.IsPMGenerated == false && proposedObj.UserId != null && proposedObj.StartDate != null && proposedObj.StopDate != null && (isNew || (proposedObj.StartDate != currentObj.StartDate) || (proposedObj.StopDate != currentObj.StopDate) || (proposedObj.UserId != currentObj.UserId) )) { if (await ct.WorkOrderItemScheduledUser.AnyAsync(x => x.Id != proposedObj.Id && x.UserId == proposedObj.UserId && x.StartDate <= proposedObj.StopDate && proposedObj.StartDate <= x.StopDate)) { AddError(ApiErrorCode.VALIDATION_FAILED, "StartDate", await Translate("ScheduleConflict")); AddError(ApiErrorCode.VALIDATION_FAILED, "StopDate", await Translate("ScheduleConflict")); } } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemScheduledUser.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void ScheduledUserValidateCanDelete(WorkOrderItemScheduledUser obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsRestrictedType) { //Scheduled Users: view only where they are the selected User and convert to labor record AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemScheduledUser)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task ScheduledUserHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemScheduledUser oProposed = (WorkOrderItemScheduledUser)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name field itself oProposed.Tags = WorkorderInfo.Tags; //for notification purposes because has no tag field itself //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT //## CREATED / UPDATED - ScheduledOnWorkorder event //Note: scheduled on workorder is immediate so same process regardless if modified or updated //because modified changes nearly all affect user so decision is just send it no matter what as any difference is enough to send if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified) { //this block is entirely about //ScheduledOnWorkorder event if (oProposed.UserId != null) { //Conditions: userid match and tags //delivery is immediate so no need to remove old ones of this kind var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorder && z.UserId == oProposed.UserId).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.ScheduledOnWorkorder, UserId = sub.UserId, AyaType = AyaType.WorkOrderItemScheduledUser, ObjectId = oProposed.Id, NotifySubscriptionId = sub.Id, Name = $"{WorkorderInfo.Serial.ToString()}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//ScheduledOnWorkorder } //--------------------------------------------------------------------------------------------------------------------------------------------- //## CREATED if (ayaEvent == AyaEvent.Created) { //ScheduledOnWorkorderImminent if (oProposed.UserId != null && oProposed.StartDate != null) { //Conditions: userid match and tags + time delayed age value //delivery is delayed so need to remove old ones of this kind on update var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorderImminent && z.UserId == oProposed.UserId).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.ScheduledOnWorkorderImminent, UserId = sub.UserId, AyaType = AyaType.WorkOrderItemScheduledUser, ObjectId = oProposed.Id, NotifySubscriptionId = sub.Id, EventDate = (DateTime)oProposed.StartDate, Name = $"{WorkorderInfo.Serial.ToString()}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//ScheduledOnWorkorderImminent } //## MODIFIED if (ayaEvent == AyaEvent.Modified) { //ScheduledOnWorkorderImminent //Always clear any old ones for this object as they are all irrelevant the moment changed: await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.ScheduledOnWorkorderImminent); if (oProposed.UserId != null && oProposed.StartDate != null) { //Conditions: userid match and tags + time delayed age value //delivery is delayed so need to remove old ones of this kind on update var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorderImminent && z.UserId == oProposed.UserId).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.ScheduledOnWorkorderImminent, UserId = sub.UserId, AyaType = AyaType.WorkOrderItemScheduledUser, ObjectId = oProposed.Id, NotifySubscriptionId = sub.Id, EventDate = (DateTime)oProposed.StartDate, Name = $"{WorkorderInfo.Serial.ToString()}" }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } }//ScheduledOnWorkorderImminent } }//end of process notifications #endregion work order item SCHEDULED USER level /* ████████╗ █████╗ ███████╗██╗ ██╗ ╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ ██║ ███████║███████╗█████╔╝ ██║ ██╔══██║╚════██║██╔═██╗ ██║ ██║ ██║███████║██║ ██╗ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ */ #region WorkOrderItemTask level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task TaskExistsAsync(long id) { return await ct.WorkOrderItemTask.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task TaskCreateAsync(WorkOrderItemTask newObject) { await TaskValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.WorkOrderItemTask.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await TaskSearchIndexAsync(newObject, true); await TaskPopulateVizFields(newObject); await TaskHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task TaskGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.WorkOrderItemTask.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task TaskPutAsync(WorkOrderItemTask putObject) { WorkOrderItemTask dbObject = await TaskGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await TaskValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await TaskExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await TaskSearchIndexAsync(dbObject, false); await TaskPopulateVizFields(putObject); await TaskHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task TaskDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await TaskGetAsync(id, false); TaskValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemTask.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await TaskHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task TaskSearchIndexAsync(WorkOrderItemTask obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Task);//some are manually entered so this is worthwhile for that at least, also I guess predefined tasks that are more rare if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task TaskGetSearchResultSummary(long id) { var obj = await TaskGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Task); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task TaskPopulateVizFields(WorkOrderItemTask o, List taskCompletionTypeEnumList = null) { if (o.CompletedByUserId != null) o.CompletedByUserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.CompletedByUserId).Select(x => x.Name).FirstOrDefaultAsync(); if (taskCompletionTypeEnumList == null) taskCompletionTypeEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList( StringUtil.TrimTypeName(typeof(WorkOrderItemTaskCompletionType).ToString()), UserTranslationId, CurrentUserRoles); o.StatusViz = taskCompletionTypeEnumList.Where(x => x.Id == (long)o.Status).Select(x => x.Name).First(); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task TaskValidateAsync(WorkOrderItemTask proposedObj, WorkOrderItemTask currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (isNew && UserIsRestrictedType) { //restricted users are not allowed to make new task entries only fill them out AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (!isNew && UserIsRestrictedType) { //Existing record so just make sure they haven't changed the not changeable fields from the db version //* Tasks: view and edit existing tasks, set completion type and date only, no add or remove or changing other fields //note that UI will prevent this, this rule is only backup for 3rd party api users if (currentObj.Task != proposedObj.Task) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Task"); if (currentObj.CompletedByUserId != UserId) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "CompletedByUserId"); if (currentObj.Sequence != proposedObj.Sequence) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Sequence"); } if (string.IsNullOrWhiteSpace(proposedObj.Task)) AddError(ApiErrorCode.VALIDATION_REQUIRED, "Task"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemTask.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void TaskValidateCanDelete(WorkOrderItemTask obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsRestrictedType) { //restricted users are not allowed to delete a task only fill them out AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemTask)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task TaskHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemTask oProposed = (WorkOrderItemTask)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name / tags field itself oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item TASK level /* ████████╗██████╗ █████╗ ██╗ ██╗███████╗██╗ ╚══██╔══╝██╔══██╗██╔══██╗██║ ██║██╔════╝██║ ██║ ██████╔╝███████║██║ ██║█████╗ ██║ ██║ ██╔══██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██║ ██║ ██║ ██║██║ ██║ ╚████╔╝ ███████╗███████╗ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝ */ #region WorkOrderItemTravel level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task TravelExistsAsync(long id) { return await ct.WorkOrderItemTravel.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task TravelCreateAsync(WorkOrderItemTravel newObject) { await TravelValidateAsync(newObject, null); if (HasErrors) return null; else { await ct.WorkOrderItemTravel.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await TravelSearchIndexAsync(newObject, true); await TravelPopulateVizFields(newObject); await TravelHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task TravelGetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.WorkOrderItemTravel.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (UserIsRestrictedType && ret.UserId != UserId) { AddError(ApiErrorCode.NOT_AUTHORIZED); return null; } if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task TravelPutAsync(WorkOrderItemTravel putObject) { WorkOrderItemTravel dbObject = await TravelGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await TravelValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await TravelExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await TravelSearchIndexAsync(putObject, false); await TravelPopulateVizFields(putObject); await TravelHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task TravelDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await TravelGetAsync(id, false); TravelValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemTravel.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await TravelHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task TravelSearchIndexAsync(WorkOrderItemTravel obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.TravelDetails); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task TravelGetSearchResultSummary(long id) { var obj = await TravelGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.TravelDetails); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task TravelPopulateVizFields(WorkOrderItemTravel o, bool calculateTotalsOnly = false) { if (calculateTotalsOnly == false) { if (o.UserId != null) o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(); } TravelRate Rate = null; if (o.TravelRateId != null) { Rate = await ct.TravelRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.TravelRateId); o.TravelRateViz = Rate.Name; } TaxCode Tax = null; if (o.TaxCodeSaleId != null) Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId); if (Tax != null) o.TaxCodeViz = Tax.Name; o.PriceViz = 0; if (Rate != null) { o.CostViz = Rate.Cost; o.ListPriceViz = Rate.Charge; o.UnitOfMeasureViz = Rate.Unit; o.PriceViz = Rate.Charge;//default price used if not manual or contract override } //manual price overrides anything if (o.PriceOverride != null) o.PriceViz = (decimal)o.PriceOverride; else { //not manual so could potentially have a contract adjustment var c = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId); if (c != null) { decimal pct = 0; ContractOverrideType cot = ContractOverrideType.PriceDiscount; bool TaggedAdjustmentInEffect = false; //POTENTIAL CONTRACT ADJUSTMENTS //First check if there is a matching tagged Travel rate contract discount, that takes precedence if (c.ContractTravelRateOverrideItems.Count > 0) { //Iterate all contract tagged items in order of ones with the most tags first foreach (var csr in c.ContractTravelRateOverrideItems.OrderByDescending(z => z.Tags.Count)) if (csr.Tags.All(z => Rate.Tags.Any(x => x == z))) { if (csr.OverridePct != 0) { pct = csr.OverridePct / 100; cot = csr.OverrideType; TaggedAdjustmentInEffect = true; } } } //Generic discount? if (!TaggedAdjustmentInEffect && c.TravelRatesOverridePct != 0) { pct = c.TravelRatesOverridePct / 100; cot = c.TravelRatesOverrideType; } //apply if discount found if (pct != 0) { if (cot == ContractOverrideType.CostMarkup) o.PriceViz = MoneyUtil.Round(o.CostViz + (o.CostViz * pct)); else if (cot == ContractOverrideType.PriceDiscount) o.PriceViz = MoneyUtil.Round(o.ListPriceViz - (o.ListPriceViz * pct)); } } } //Calculate totals and taxes //NET o.NetViz = MoneyUtil.Round(o.PriceViz * o.TravelRateQuantity); //TAX o.TaxAViz = 0; o.TaxBViz = 0; if (Tax != null) { if (Tax.TaxAPct != 0) { o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100)); } if (Tax.TaxBPct != 0) { if (Tax.TaxOnTax) { o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100)); } else { o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100)); } } } o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz; //RESTRICTIONS ON COST VISIBILITY? if (!UserCanViewLaborOrTravelRateCosts) { o.CostViz = 0; } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task TravelValidateAsync(WorkOrderItemTravel proposedObj, WorkOrderItemTravel currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId))) { //no edits allowed on other people's records AddError(ApiErrorCode.NOT_AUTHORIZED); return; } if (proposedObj.TravelRateQuantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "TravelRateQuantity"); if (proposedObj.NoChargeQuantity < 0)//negative quantities are not allowed AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "NoChargeQuantity"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemTravel.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields } } private void TravelValidateCanDelete(WorkOrderItemTravel obj) { if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } if (UserIsRestrictedType) { //Travels: add (no user selection defaults to themselves), remove, view and edit only when they are the selected User if (obj.UserId != UserId) AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemTravel)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task TravelHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemTravel oProposed = (WorkOrderItemTravel)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString();//for notification purposes because has no name / tags field itself oProposed.Tags = WorkorderInfo.Tags; //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item TRAVEL level /* ██╗ ██╗███╗ ██╗██╗████████╗ ██║ ██║████╗ ██║██║╚══██╔══╝ ██║ ██║██╔██╗ ██║██║ ██║ ██║ ██║██║╚██╗██║██║ ██║ ╚██████╔╝██║ ╚████║██║ ██║ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ */ #region WorkOrderItemUnit level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task UnitExistsAsync(long id) { return await ct.WorkOrderItemUnit.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task UnitCreateAsync(WorkOrderItemUnit newObject) { await UnitValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemUnit.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct); await UnitSearchIndexAsync(newObject, true); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); await UnitPopulateVizFields(newObject, false); await UnitHandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task UnitGetAsync(long id, bool logTheGetEvent = true) { if (UserIsSubContractorRestricted) //no access allowed at all return null; var ret = await ct.WorkOrderItemUnit.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task UnitPutAsync(WorkOrderItemUnit putObject) { WorkOrderItemUnit dbObject = await UnitGetAsync(putObject.Id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } if (dbObject.Concurrency != putObject.Concurrency) { AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); await UnitValidateAsync(putObject, dbObject); if (HasErrors) return null; ct.Replace(dbObject, putObject); try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await UnitExistsAsync(putObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else AddError(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct); await UnitSearchIndexAsync(putObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); await UnitPopulateVizFields(putObject, false); await UnitHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject); return putObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task UnitDeleteAsync(long id, IDbContextTransaction parentTransaction = null) { var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync(); var dbObject = await UnitGetAsync(id, false); UnitValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemUnit.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix?? await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct); if (parentTransaction == null) await transaction.CommitAsync(); await UnitHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject); return true; } ////////////////////////////////////////////// //INDEXING // private async Task UnitSearchIndexAsync(WorkOrderItemUnit obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task UnitGetSearchResultSummary(long id) { var obj = await UnitGetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VIZ POPULATE // private async Task UnitPopulateVizFields(WorkOrderItemUnit o, bool populateForReporting) { var unitInfo = await ct.Unit.AsNoTracking() .Where(x => x.Id == o.UnitId) .Select(x => new { x.Serial, x.Description, x.UnitModelId, x.Address, x.City, x.Region, x.Country, x.Latitude, x.Longitude }) .FirstOrDefaultAsync(); o.UnitViz = unitInfo.Serial; o.UnitDescriptionViz = unitInfo.Description; if (populateForReporting) { o.AddressViz = unitInfo.Address; o.CityViz = unitInfo.City; o.RegionViz = unitInfo.Region; o.CountryViz = unitInfo.Country; o.LatitudeViz = unitInfo.Latitude; o.LongitudeViz = unitInfo.Longitude; } if (unitInfo.UnitModelId != null) { var unitModelInfo = await ct.UnitModel.AsNoTracking().Where(x => x.Id == unitInfo.UnitModelId).Select(x => new { x.Name, x.VendorId, x.Number }).FirstOrDefaultAsync(); o.UnitModelNameViz = unitModelInfo.Name; o.UnitModelModelNumberViz = unitModelInfo.Number; if (unitModelInfo.VendorId != null) o.UnitModelVendorViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == unitModelInfo.VendorId).Select(x => x.Name).FirstOrDefaultAsync(); } } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task UnitValidateAsync(WorkOrderItemUnit proposedObj, WorkOrderItemUnit currentObj) { //skip validation if seeding // if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; //run validation and biz rules bool isNew = currentObj == null; if (UserIsRestrictedType) { //Units: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (proposedObj.WorkOrderItemId == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); return;//this is a completely disqualifying error } else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); return;//this is a completely disqualifying error } //Check state if updatable right now if (!isNew) { //Front end is coded to save the state first before any other updates if it has changed and it would not be //a part of this header update so it's safe to check it here as it will be most up to date var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id); if (CurrentWoStatus.Locked) { AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked")); return;//this is a completely disqualifying error } } if (proposedObj.UnitId < 1 || !await ct.Unit.AnyAsync(x => x.Id == proposedObj.UnitId)) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId"); //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemUnit.ToString()); if (FormCustomization != null) { //Yeppers, do the validation, there are two, the custom fields and the regular fields that might be set to required //validate users choices for required non custom fields RequiredFieldsValidator.Validate(this, FormCustomization, proposedObj);//note: this is passed only to add errors //validate custom fields CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void UnitValidateCanDelete(WorkOrderItemUnit obj) { if (UserIsRestrictedType) { //Units: no edits allowed AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror"); return; } if (obj == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemUnit)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task UnitHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]"); bool isNew = currentObj == null; WorkOrderItemUnit oProposed = (WorkOrderItemUnit)proposedObj; var wid = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct); var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync(); oProposed.Name = WorkorderInfo.Serial.ToString();//for notification purposes because has no name field itself //STANDARD EVENTS FOR ALL OBJECTS await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); //SPECIFIC EVENTS FOR THIS OBJECT }//end of process notifications #endregion work order item LABOR level #region Utility public async Task GetWorkOrderGraphItem(AyaType ayaType, long id) { switch (ayaType) { case AyaType.WorkOrder: return await WorkOrderGetAsync(id, false) as ICoreBizObjectModel; case AyaType.WorkOrderItem: return await ItemGetAsync(id, false); case AyaType.WorkOrderItemExpense: return await ExpenseGetAsync(id, false); case AyaType.WorkOrderItemLabor: return await LaborGetAsync(id, false); case AyaType.WorkOrderItemLoan: return await LoanGetAsync(id, false); case AyaType.WorkOrderItemPart: return await PartGetAsync(id, false); case AyaType.WorkOrderItemPartRequest: return await PartRequestGetAsync(id, false); case AyaType.WorkOrderItemScheduledUser: return await ScheduledUserGetAsync(id, false); case AyaType.WorkOrderItemTask: return await TaskGetAsync(id, false); case AyaType.WorkOrderItemTravel: return await TravelGetAsync(id, false); case AyaType.WorkOrderItemUnit: return await UnitGetAsync(id, false); case AyaType.WorkOrderItemOutsideService: return await OutsideServiceGetAsync(id, false); default: throw new System.ArgumentOutOfRangeException($"WorkOrder::GetWorkOrderGraphItem -> Invalid ayaType{ayaType}"); } } public async Task PutWorkOrderGraphItem(AyaType ayaType, ICoreBizObjectModel o) { ClearErrors(); switch (ayaType) { case AyaType.WorkOrder: if (o is WorkOrder) { WorkOrder dto = new WorkOrder(); CopyObject.Copy(o, dto); return await WorkOrderPutAsync((WorkOrder)dto); } return await WorkOrderPutAsync((WorkOrder)o) as ICoreBizObjectModel; case AyaType.WorkOrderItem: if (o is WorkOrderItem) { WorkOrderItem dto = new WorkOrderItem(); CopyObject.Copy(o, dto); return await ItemPutAsync((WorkOrderItem)dto); } return await ItemPutAsync((WorkOrderItem)o); case AyaType.WorkOrderItemExpense: return await ExpensePutAsync((WorkOrderItemExpense)o); case AyaType.WorkOrderItemLabor: return await LaborPutAsync((WorkOrderItemLabor)o); case AyaType.WorkOrderItemLoan: return await LoanPutAsync((WorkOrderItemLoan)o); case AyaType.WorkOrderItemPart: return await PartPutAsync((WorkOrderItemPart)o); case AyaType.WorkOrderItemPartRequest: return await PartRequestPutAsync((WorkOrderItemPartRequest)o); case AyaType.WorkOrderItemScheduledUser: return await ScheduledUserPutAsync((WorkOrderItemScheduledUser)o); case AyaType.WorkOrderItemTask: return await TaskPutAsync((WorkOrderItemTask)o); case AyaType.WorkOrderItemTravel: return await TravelPutAsync((WorkOrderItemTravel)o); case AyaType.WorkOrderItemUnit: return await UnitPutAsync((WorkOrderItemUnit)o); case AyaType.WorkOrderItemOutsideService: return await OutsideServicePutAsync((WorkOrderItemOutsideService)o); default: throw new System.ArgumentOutOfRangeException($"WorkOrder::PutWorkOrderGraphItem -> Invalid ayaType{ayaType}"); } } public async Task DeleteWorkOrderGraphItem(AyaType ayaType, long id) { switch (ayaType) { case AyaType.WorkOrder: return await WorkOrderDeleteAsync(id); case AyaType.WorkOrderItem: return await ItemDeleteAsync(id); case AyaType.WorkOrderItemExpense: return await ExpenseDeleteAsync(id); case AyaType.WorkOrderItemLabor: return await LaborDeleteAsync(id); case AyaType.WorkOrderItemLoan: return await LoanDeleteAsync(id); case AyaType.WorkOrderItemPart: return await PartDeleteAsync(id); case AyaType.WorkOrderItemPartRequest: return await PartRequestDeleteAsync(id); case AyaType.WorkOrderItemScheduledUser: return await ScheduledUserDeleteAsync(id); case AyaType.WorkOrderItemTask: return await TaskDeleteAsync(id); case AyaType.WorkOrderItemTravel: return await TravelDeleteAsync(id); case AyaType.WorkOrderItemUnit: return await UnitDeleteAsync(id); case AyaType.WorkOrderItemOutsideService: return await OutsideServiceDeleteAsync(id); default: throw new System.ArgumentOutOfRangeException($"WorkOrder::GetWorkOrderGraphItem -> Invalid ayaType{ayaType}"); } } //////////////////////////////////////////////////////////////////////////////////////////////// //GET CONTRACT FOR WORKORDER FROM RELATIVE // //cache the contract to save repeatedly fetching it for this operation internal Contract mContractInEffect = null; internal bool mFetchedContractAlready = false;//null contract isn't enough to know it was fetched as it could just not have a contract so this is required internal async Task GetCurrentWorkOrderContractFromRelatedAsync(AyaType ayaType, long id) { if (mFetchedContractAlready == false) { var wid = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct); var WoContractId = await ct.WorkOrder.AsNoTracking().Where(z => z.Id == wid.ParentId).Select(z => z.ContractId).FirstOrDefaultAsync(); await GetCurrentContractFromContractIdAsync(WoContractId); } return mContractInEffect; } internal async Task GetCurrentContractFromContractIdAsync(long? id) { if (id == null) return null; if (mFetchedContractAlready == false) { mContractInEffect = await GetFullyPopulatedContractGraphFromIdAsync(id); } return mContractInEffect; } internal async Task GetFullyPopulatedContractGraphFromIdAsync(long? id) { if (id == null) return null; return await ct.Contract.AsSplitQuery().AsNoTracking() .Include(c => c.ServiceRateItems) .Include(c => c.TravelRateItems) .Include(c => c.ContractPartOverrideItems) .Include(c => c.ContractTravelRateOverrideItems) .Include(c => c.ContractServiceRateOverrideItems) .FirstOrDefaultAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //GET CURRENT STATUS FOR WORKORDER FROM RELATIVE // //cache the state to save repeatedly fetching it for this operation which could be called multiple times in a flowv internal WorkOrderStatus mCurrentWorkOrderStatus = null; internal async Task GetCurrentWorkOrderStatusFromRelatedAsync(AyaType ayaType, long id) { if (mCurrentWorkOrderStatus == null) { var wid = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct); var stat = await ct.WorkOrderState.AsNoTracking() .Where(z => z.WorkOrderId == wid.ParentId) .OrderByDescending(z => z.Created) .Take(1) .FirstOrDefaultAsync(); //no state set yet? if (stat == null) mCurrentWorkOrderStatus = new WorkOrderStatus() { Id = -1, Locked = false, Completed = false }; else mCurrentWorkOrderStatus = await ct.WorkOrderStatus.AsNoTracking().Where(z => z.Id == stat.WorkOrderStatusId).FirstAsync();//this should never not be null } return mCurrentWorkOrderStatus; } // internal static async Task GetCurrentWorkOrderStatusFromRelatedAsync(AyaType ayaType, long id, AyContext ct) // { // //static method // var wid = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct); // var stat = await ct.WorkOrderState.AsNoTracking() // .Where(z => z.WorkOrderId == wid.WorkOrderId) // .OrderByDescending(z => z.Created) // .Take(1) // .FirstOrDefaultAsync(); // //no state set yet? // if (stat == null) // { //default // return new WorkOrderStatus() { Id = -1, Locked = false, Completed = false }; // } // return await ct.WorkOrderStatus.AsNoTracking().Where(z => z.Id == stat.WorkOrderStatusId).FirstAsync();//this should never not be null // } #endregion utility ///////////////////////////////////////////////////////////////////// }//eoc }//eons