using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Models; using System.Linq; namespace AyaNova.Biz { internal class WorkOrderBiz : BizObject, ISearchAbleObject { //Feature specific roles internal static AuthorizationRoles RolesAllowedToChangeSerial = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.AccountingFull; internal WorkOrderBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = UserRoles; BizType = AyaType.WorkOrder; } 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)); else//when called internally for internal ops there will be no context so need to set default values for that return new WorkOrderBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); } /* ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗ ██║ █╗ ██║██║ ██║██████╔╝█████╔╝█████╗██║ ██║██████╔╝██║ ██║█████╗ ██████╔╝ ██║███╗██║██║ ██║██╔══██╗██╔═██╗╚════╝██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗ ╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ ╚██████╔╝██║ ██║██████╔╝███████╗██║ ██║ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝ */ #region WorkOrder level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task WorkOrderExistsAsync(long id) { return await ct.WorkOrder.AnyAsync(e => e.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task CreateAsync(dtWorkOrder dtNewObject) { WorkOrder newObject = new WorkOrder(); CopyObject.Copy(dtNewObject, newObject); await ValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Serial = ServerBootConfig.WORKORDER_SERIAL.GetNext(); newObject.Tags = TagUtil.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 SearchIndexAsync(newObject, true); await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// //DUPLICATE // internal async Task DuplicateAsync(long id) { WorkOrder dbObject = await GetEntireWorkOrderAsync(id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } WorkOrder newObject = new WorkOrder(); CopyObject.Copy(dbObject, newObject, "Wiki"); newObject.Serial = ServerBootConfig.WORKORDER_SERIAL.GetNext(); newObject.Id = 0; newObject.ConcurrencyToken = 0; await ct.WorkOrder.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await SearchIndexAsync(newObject, true); await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); return newObject; } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task GetEntireWorkOrderAsync(long id, bool logTheGetEvent = true) { //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.WorkOrder .Include(w => w.Items) .ThenInclude(wi => wi.Labors) .Include(w => w.Items) .ThenInclude(wi => wi.Parts) .SingleOrDefaultAsync(m => m.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PutAsync(dtWorkOrder dtPutObject) { WorkOrder dbObject = await ct.WorkOrder.SingleOrDefaultAsync(m => m.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrder SnapshotOfOriginalDBObj = new WorkOrder(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); //Replace the db object with the PUT object CopyObject.Copy(dtPutObject, dbObject, "Id,Serial"); //if user has rights then change it, otherwise just ignore it and do the rest if (dtPutObject.Serial != 0 && SnapshotOfOriginalDBObj.Serial != dtPutObject.Serial && Authorized.HasAnyRole(CurrentUserRoles, RolesAllowedToChangeSerial)) { dbObject.Serial = dtPutObject.Serial; } dbObject.Tags = TagUtil.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["ConcurrencyToken"] = dtPutObject.ConcurrencyToken; await ValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await WorkOrderExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, BizType, AyaEvent.Modified), ct); await SearchIndexAsync(dbObject, false); await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task DeleteAsync(long id) { using (var transaction = await ct.Database.BeginTransactionAsync()) { try { WorkOrder dbObject = await ct.WorkOrder.SingleOrDefaultAsync(m => m.Id == id); ValidateCanDelete(dbObject); if (HasErrors) return false; //collect the child id's to delete var ItemIds = await ct.WorkOrderItem.Where(m => m.WorkOrderId == id).Select(m => m.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, BizType, dbObject.Id, dbObject.Serial.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType); await TagUtil.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); #if (DEBUG) if (dbObject.Wiki == "INTEGRATION_DELETE_TEST_FAIL_BEFORE_COMMIT") { throw new System.Exception("WorkorderBiz::Delete - Test exception"); } #endif //all good do the commit await transaction.CommitAsync(); } catch { //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } return true; } } private async Task SearchIndexAsync(WorkOrder obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, BizType); SearchParams.AddText(obj.Notes).AddText(obj.Serial).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task GetSearchResultSummary(long id) { var obj = await ct.WorkOrder.SingleOrDefaultAsync(m => m.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Serial).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // //Can save or update? private async Task ValidateAsync(WorkOrder proposedObj, WorkOrder currentObj) { //run validation and biz rules bool isNew = currentObj == null; // //Name required // if (string.IsNullOrWhiteSpace(proposedObj.Name)) // AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name"); // //Name must be less than 255 characters // if (proposedObj.Name.Length > 255) // AddError(ApiErrorCode.VALIDATION_LENGTH_EXCEEDED, "Name", "255 max"); // //If name is otherwise OK, check that name is unique // if (!PropertyHasErrors("Name")) // { // //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false // if (await ct.WorkOrder.AnyAsync(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id)) // { // AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name"); // } // } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.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 ValidateCanDelete(WorkOrder dbObject) { //whatever needs to be check to delete this object } #endregion workorder level /* ██╗████████╗███████╗███╗ ███╗███████╗ ██║╚══██╔══╝██╔════╝████╗ ████║██╔════╝ ██║ ██║ █████╗ ██╔████╔██║███████╗ ██║ ██║ ██╔══╝ ██║╚██╔╝██║╚════██║ ██║ ██║ ███████╗██║ ╚═╝ ██║███████║ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝ */ #region WorkOrderItem level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ItemExistsAsync(long id) { return await ct.WorkOrderItem.AnyAsync(e => e.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task CreateItemAsync(dtWorkOrderItem dtNewObject) { WorkOrderItem newObject = new WorkOrderItem(); CopyObject.Copy(dtNewObject, newObject); await ItemValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagUtil.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 TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task GetItemAsync(long id, bool logTheGetEvent = true) { //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 .Include(wi => wi.Labors) .Include(wi => wi.Parts) .SingleOrDefaultAsync(m => m.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(dtWorkOrderItem dtPutObject) { WorkOrderItem dbObject = await ct.WorkOrderItem.SingleOrDefaultAsync(m => m.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItem SnapshotOfOriginalDBObj = new WorkOrderItem(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); //Replace the db object with the PUT object CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagUtil.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["ConcurrencyToken"] = dtPutObject.ConcurrencyToken; await ItemValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await ItemExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItem, AyaEvent.Modified), ct); await ItemSearchIndexAsync(dbObject, false); await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task ItemDeleteAsync(long id, Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction parentTransaction = null) { Microsoft.EntityFrameworkCore.Storage.IDbContextTransaction transaction = null; if (parentTransaction == null) transaction = await ct.Database.BeginTransactionAsync(); try { WorkOrderItem dbObject = await ct.WorkOrderItem.SingleOrDefaultAsync(m => m.Id == id); ItemValidateCanDelete(dbObject); if (HasErrors) return false; //collect the child id's to delete var LaborIds = await ct.WorkOrderItemLabor.Where(m => m.WorkOrderItemId == id).Select(m => m.Id).ToListAsync(); var PartIds = await ct.WorkOrderItemPart.Where(m => m.WorkOrderItemId == id).Select(m => m.Id).ToListAsync(); //Delete children foreach (long ItemId in LaborIds) { if (!await LaborDeleteAsync(ItemId)) return false; } foreach (long ItemId in PartIds) { if (!await PartDeleteAsync(ItemId)) return false; } ct.WorkOrderItem.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItem, dbObject.Id, "wo:" + dbObject.WorkOrderId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItem); await TagUtil.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); //all good do the commit if it's ours if (parentTransaction == null) await transaction.CommitAsync(); } catch { //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } finally { if (parentTransaction == null) await transaction.DisposeAsync(); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //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"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.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; } //re-check rights here necessary due to traversal delete from Principle object if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItem)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } 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.SingleOrDefaultAsync(m => m.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item level /* ██╗ █████╗ ██████╗ ██████╗ ██████╗ ██║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗ ██║ ███████║██████╔╝██║ ██║██████╔╝ ██║ ██╔══██║██╔══██╗██║ ██║██╔══██╗ ███████╗██║ ██║██████╔╝╚██████╔╝██║ ██║ ╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝ */ #region WorkOrderItemLabor level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task LaborExistsAsync(long id) { return await ct.WorkOrderItemLabor.AnyAsync(e => e.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task LaborCreateAsync(WorkOrderItemLabor newObject) { await LaborValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagUtil.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemLabor.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemLabor, AyaEvent.Created), ct); await LaborSearchIndexAsync(newObject, true); await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task LaborGetAsync(long id, bool logTheGetEvent = true) { //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.WorkOrderItemLabor .SingleOrDefaultAsync(m => m.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemLabor, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task LaborPutAsync(WorkOrderItemLabor dtPutObject) { WorkOrderItemLabor dbObject = await ct.WorkOrderItemLabor.SingleOrDefaultAsync(m => m.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemLabor SnapshotOfOriginalDBObj = new WorkOrderItemLabor(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagUtil.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["ConcurrencyToken"] = dtPutObject.ConcurrencyToken; await LaborValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await LaborExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemLabor, AyaEvent.Modified), ct); await LaborSearchIndexAsync(dbObject, false); await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task LaborDeleteAsync(long id) { WorkOrderItemLabor dbObject = await ct.WorkOrderItemLabor.SingleOrDefaultAsync(m => m.Id == id); LaborValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemLabor.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemLabor, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemLabor); await TagUtil.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task LaborValidateAsync(WorkOrderItemLabor proposedObj, WorkOrderItemLabor currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.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 CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void LaborValidateCanDelete(WorkOrderItemLabor obj) { 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.WorkOrderItemLabor)) { AddError(ApiErrorCode.NOT_AUTHORIZED); return; } } ////////////////////////////////////////////// //INDEXING // private async Task LaborSearchIndexAsync(WorkOrderItemLabor obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemLabor); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task LaborGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemLabor.SingleOrDefaultAsync(m => m.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level /* ██████╗ █████╗ ██████╗ ████████╗███████╗ ██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝ ██████╔╝███████║██████╔╝ ██║ ███████╗ ██╔═══╝ ██╔══██║██╔══██╗ ██║ ╚════██║ ██║ ██║ ██║██║ ██║ ██║ ███████║ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ */ #region WorkOrderItemPart level //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task PartExistsAsync(long id) { return await ct.WorkOrderItemPart.AnyAsync(e => e.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task CreatePartAsync(WorkOrderItemPart newObject) { await PartValidateAsync(newObject, null); if (HasErrors) return null; else { newObject.Tags = TagUtil.NormalizeTags(newObject.Tags); newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); await ct.WorkOrderItemPart.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItemPart, AyaEvent.Created), ct); await PartSearchIndexAsync(newObject, true); await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// // GET // internal async Task GetPartAsync(long id, bool logTheGetEvent = true) { //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.WorkOrderItemPart .SingleOrDefaultAsync(m => m.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemPart, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // internal async Task PartPutAsync(WorkOrderItemPart dtPutObject) { WorkOrderItemPart dbObject = await ct.WorkOrderItemPart.SingleOrDefaultAsync(m => m.Id == dtPutObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } // make a snapshot of the original for validation but update the original to preserve workflow WorkOrderItemPart SnapshotOfOriginalDBObj = new WorkOrderItemPart(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(dtPutObject, dbObject, "Id"); dbObject.Tags = TagUtil.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["ConcurrencyToken"] = dtPutObject.ConcurrencyToken; await PartValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; try { await ct.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!await PartExistsAsync(dtPutObject.Id)) AddError(ApiErrorCode.NOT_FOUND); else new ApiErrorResponse(ApiErrorCode.CONCURRENCY_CONFLICT); return null; } await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, AyaType.WorkOrderItemPart, AyaEvent.Modified), ct); await PartSearchIndexAsync(dbObject, false); await TagUtil.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task PartDeleteAsync(long id) { WorkOrderItemPart dbObject = await ct.WorkOrderItemPart.SingleOrDefaultAsync(m => m.Id == id); PartValidateCanDelete(dbObject); if (HasErrors) return false; ct.WorkOrderItemPart.Remove(dbObject); await ct.SaveChangesAsync(); //Log event await EventLogProcessor.DeleteObjectLogAsync(UserId, AyaType.WorkOrderItemPart, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct); await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, AyaType.WorkOrderItemPart); await TagUtil.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task PartValidateAsync(WorkOrderItemPart proposedObj, WorkOrderItemPart currentObj) { //run validation and biz rules bool isNew = currentObj == null; if (proposedObj.WorkOrderItemId == 0) AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId"); else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId"); } //Any form customizations to validate? var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.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 CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields); } } private void PartValidateCanDelete(WorkOrderItemPart obj) { 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; } } private async Task PartSearchIndexAsync(WorkOrderItemPart obj, bool isNew) { //SEARCH INDEXING var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItemPart); SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); if (isNew) await Search.ProcessNewObjectKeywordsAsync(SearchParams); else await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams); } public async Task PartGetSearchResultSummary(long id) { var obj = await ct.WorkOrderItemPart.SingleOrDefaultAsync(m => m.Id == id); var SearchParams = new Search.SearchIndexProcessObjectParameters(); if (obj != null) SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields); return SearchParams; } #endregion work order item LABOR level //http://www.patorjk.com/software/taag/#p=display&f=ANSI%20Shadow&t=work-order //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // //Other job handlers here... ///////////////////////////////////////////////////////////////////// }//eoc }//eons