using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.JsonPatch; using EnumsNET; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using AyaNova.Biz; using AyaNova.Models; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; namespace AyaNova.Biz { internal class UserBiz : BizObject, IJobObject, IImportAyaNova7Object { private readonly AyContext ct; public readonly long userId; private readonly AuthorizationRoles userRoles; public bool V7ValidationImportMode { get; set; } internal UserBiz(AyContext dbcontext, long currentUserId, AuthorizationRoles UserRoles) { ct = dbcontext; userId = currentUserId; userRoles = UserRoles; V7ValidationImportMode = false;//default } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE internal async Task CreateAsync(User inObj) { Validate(inObj, true); if (HasErrors) return null; else { //do stuff with User User outObj = inObj; outObj.OwnerId = userId; //SearchHelper(break down text fields, save to db) //TagHelper(collection of tags??) await ct.User.AddAsync(outObj); return outObj; } } //////////////////////////////////////////////////////////////////////////////////////////////// /// GET //Get one internal async Task GetAsync(long fetchId) { //This is simple so nothing more here, but often will be copying to a different output object or some other ops return await ct.User.SingleOrDefaultAsync(m => m.Id == fetchId); } //get many (paged) internal async Task> GetManyAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions) { pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset; pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit; var items = await ct.User .OrderBy(m => m.Id) .Skip(pagingOptions.Offset.Value) .Take(pagingOptions.Limit.Value) .ToArrayAsync(); var totalRecordCount = await ct.User.CountAsync(); var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject(); ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks); return pr; } //get picklist (paged) internal async Task> GetPickListAsync(IUrlHelper Url, string routeName, PagingOptions pagingOptions, string q) { pagingOptions.Offset = pagingOptions.Offset ?? PagingOptions.DefaultOffset; pagingOptions.Limit = pagingOptions.Limit ?? PagingOptions.DefaultLimit; NameIdItem[] items; int totalRecordCount = 0; if (!string.IsNullOrWhiteSpace(q)) { items = await ct.User .Where(m => EF.Functions.ILike(m.Name, q)) .OrderBy(m => m.Name) .Skip(pagingOptions.Offset.Value) .Take(pagingOptions.Limit.Value) .Select(m => new NameIdItem() { Id = m.Id, Name = m.Name }).ToArrayAsync(); totalRecordCount = await ct.User.Where(m => EF.Functions.ILike(m.Name, q)).CountAsync(); } else { items = await ct.User .OrderBy(m => m.Name) .Skip(pagingOptions.Offset.Value) .Take(pagingOptions.Limit.Value) .Select(m => new NameIdItem() { Id = m.Id, Name = m.Name }).ToArrayAsync(); totalRecordCount = await ct.User.CountAsync(); } var pageLinks = new PaginationLinkBuilder(Url, routeName, null, pagingOptions, totalRecordCount).PagingLinksObject(); ApiPagedResponse pr = new ApiPagedResponse(items, pageLinks); return pr; } //////////////////////////////////////////////////////////////////////////////////////////////// //UPDATE // //put internal bool Put(User dbObj, User inObj) { //Replace the db object with the PUT object CopyObject.Copy(inObj, dbObj, "Id"); //Set "original" value of concurrency token to input token //this will allow EF to check it out ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = inObj.ConcurrencyToken; Validate(dbObj, false); if (HasErrors) return false; return true; } //patch internal bool Patch(User dbObj, JsonPatchDocument objectPatch, uint concurrencyToken) { //Do the patching objectPatch.ApplyTo(dbObj); ct.Entry(dbObj).OriginalValues["ConcurrencyToken"] = concurrencyToken; Validate(dbObj, false); if (HasErrors) return false; return true; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal bool Delete(User dbObj) { //Determine if the object can be deleted, do the deletion tentatively //Probably also in here deal with tags and associated search text etc ValidateCanDelete(dbObj); if (HasErrors) return false; ct.User.Remove(dbObj); return true; } /// /// Delete child objects like tags and attachments and etc /// /// internal void DeleteChildren(User dbObj) { //TAGS TagMapBiz.DeleteAllForObject(new AyaTypeId(AyaType.User, dbObj.Id), ct); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // //Can save or update? private void Validate(User inObj, bool isNew) { //run validation and biz rules if (isNew) { //NEW Users must be active if (((bool)inObj.Active) == false) { AddError(ValidationErrorType.InvalidValue, "Active", "New User must be active"); } } //OwnerId required if (!isNew) { if (inObj.OwnerId == 0) AddError(ValidationErrorType.RequiredPropertyEmpty, "OwnerId"); } //Name required if (string.IsNullOrWhiteSpace(inObj.Name)) AddError(ValidationErrorType.RequiredPropertyEmpty, "Name"); //Name must be less than 255 characters if (inObj.Name.Length > 255) AddError(ValidationErrorType.LengthExceeded, "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 (ct.User.Any(m => m.Name == inObj.Name && m.Id != inObj.Id)) { AddError(ValidationErrorType.NotUnique, "Name"); } } if (!inObj.UserType.IsValid()) { AddError(ValidationErrorType.InvalidValue, "UserType"); } //Validate client type user if (!V7ValidationImportMode && inObj.UserType == UserType.Client) { if (inObj.ClientId == null || inObj.ClientId == 0) { AddError(ValidationErrorType.RequiredPropertyEmpty, "ClientId"); } else { //verify that client exists //TODO WHEN CLIENT OBJECT MADE if(!ct.Client.any(m)) } } //Validate headoffice type user if (!V7ValidationImportMode && inObj.UserType == UserType.HeadOffice) { if (inObj.HeadOfficeId == null || inObj.HeadOfficeId == 0) { AddError(ValidationErrorType.RequiredPropertyEmpty, "HeadOfficeId"); } else { //verify that HeadOfficeId exists //TODO WHEN HEADOFFICE OBJECT MADE if(!ct.HeadOffice.any(m)) } } //Validate headoffice type user if (!V7ValidationImportMode && inObj.UserType == UserType.Subcontractor) { if (inObj.SubVendorId == null || inObj.SubVendorId == 0) { AddError(ValidationErrorType.RequiredPropertyEmpty, "SubVendorId"); } else { //verify that VENDOR SubVendorId exists //TODO WHEN VENDOR OBJECT MADE if(!ct.Vendor.any(m)) } } if (!inObj.Roles.IsValid()) { AddError(ValidationErrorType.InvalidValue, "Roles"); } return; } //Can delete? private void ValidateCanDelete(User inObj) { //whatever needs to be check to delete this object } //////////////////////////////////////////////////////////////////////////////////////////////// //JOB / OPERATIONS // public async Task HandleJobAsync(OpsJob job) { //just to hide compiler warning for now await Task.CompletedTask; //Hand off the particular job to the corresponding processing code //NOTE: If this code throws an exception the caller (JobsBiz::ProcessJobsAsync) will automatically set the job to failed and log the exeption so //basically any error condition during job processing should throw up an exception if it can't be handled switch (job.JobType) { default: throw new System.ArgumentOutOfRangeException($"UserBiz.HandleJob-> Invalid job type{job.JobType.ToString()}"); } } public async Task ImportV7Async(JObject j, List importMap, Guid jobId) { #region V7 record format /* { "DefaultLanguage": "Custom English", TODO: "DefaultServiceTemplateID": "ca83a7b8-4e5f-4a7b-a02b-9cf78d5f983f", "UserType": 2, "Active": true, "ClientID": "00000000-0000-0000-0000-000000000000", "HeadOfficeID": "00000000-0000-0000-0000-000000000000", "MemberOfGroup": "0f8a80ff-4b03-4114-ae51-2d13b812dd65", "Created": "03/21/2005 07:19 AM", "Modified": "09/15/2015 12:22 PM", "Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed", "Modifier": "1d859264-3f32-462a-9b0c-a67dddfdf4d3", "ID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3", "FirstName": "Hank", "LastName": "Rearden", "Initials": "HR", "EmployeeNumber": "EMP1236", "PageAddress": "", "PageMaxText": 24, "Phone1": "", "Phone2": "", "EmailAddress": "", "UserCertifications": [ { "Created": "12/22/2005 02:07 PM", "Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed", "Modified": "12/22/2005 02:08 PM", "Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed", "ID": "4492360c-43e4-4209-9f33-30691b0808ed", "UserCertificationID": "b2f26359-7c42-4218-923a-e949f3ef1f85", "UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3", "ValidStartDate": "2005-10-11T00:00:00-07:00", "ValidStopDate": "2006-10-11T00:00:00-07:00" } ], "UserSkills": [ { "Created": "12/22/2005 02:06 PM", "Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed", "Modified": "12/22/2005 02:08 PM", "Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed", "ID": "1dc5ce96-f411-4885-856e-5bdb3ad79728", "UserSkillID": "2e6f8b65-594c-4f6c-9cd6-e14a562daba8", "UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3" }, { "Created": "12/22/2005 02:06 PM", "Creator": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed", "Modified": "12/22/2005 02:08 PM", "Modifier": "2ecc77fc-69e2-4a7e-b88d-bd0ecaf36aed", "ID": "88e476d3-7526-45f5-a0dd-706c8053a63f", "UserSkillID": "47a4ee94-b0e9-41b5-afe5-4b4f2c981877", "UserID": "1d859264-3f32-462a-9b0c-a67dddfdf4d3" } ], "Notes": "", "VendorID": "06e502c2-69ba-4e88-8efb-5b53c1687740", "RegionID": "f856423a-d468-4344-b7b8-121e466738c6", "DispatchZoneID": "00000000-0000-0000-0000-000000000000", "SubContractor": false,//This is not actually a feature in v7, you can just pick a vendorId for subcontractor but still be of type technician "DefaultWarehouseID": "d45eab37-b6e6-4ad2-9163-66d7ba83a98c", "Custom1": "", "Custom2": "", "Custom3": "", "Custom4": "", "Custom5": "", "Custom6": "", "Custom7": "", "Custom8": "", "Custom9": "", "Custom0": "", "ScheduleBackColor": -2097216, "TimeZoneOffset": null } */ #endregion v7 record format V7ValidationImportMode = true; //some types need to import from more than one source hence the seemingly redundant switch statement for futureproofing switch (j["V7_TYPE"].Value()) { case "GZTW.AyaNova.BLL.User": { var V7Id = new Guid(j["ID"].Value()); //Copy values User i = new User(); i.Name = j["FirstName"].Value() + " " + j["LastName"].Value(); var Temp = j["UserType"].Value(); i.UserType = (UserType)Temp; //If there is a vendorId set then this user is actually a subcontractor in v7 so set accordingly var VendorId = new Guid(j["VendorID"].Value()); if (VendorId != Guid.Empty) { i.UserType = UserType.Subcontractor; } i.Active = j["Active"].Value(); i.EmployeeNumber = j["EmployeeNumber"].Value(); i.Notes = j["Notes"].Value(); //Set unusable random login credentials i.Salt = Hasher.GenerateSalt(); i.Login = Hasher.GenerateSalt(); i.Password = Hasher.hash(i.Salt, Hasher.GenerateSalt()); //No rights i.Roles=AuthorizationRoles.NoRole; User o = await CreateAsync(i); if (HasErrors) { //If there are any validation errors, log in joblog and move on JobsBiz.LogJob(jobId, $" -> import object \"{i.Name}\" source id {V7Id.ToString()} failed validation and was not imported: {GetErrorsAsString()} ", ct); //This is a fundamental problem with the import as users are required for many things so bomb out entirely //other things might be able to work around but this is too serious throw new System.SystemException("UserBiz::ImportV7Async - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE WITHOUT ALL USERS BEING IMPORTED, SEE JOB ERROR LOG FOR DETAILS"); // return false; } else { await ct.SaveChangesAsync(); var mapItem = new ImportAyaNova7MapItem(V7Id, AyaType.User, o.Id); importMap.Add(mapItem); } } break; case "GZTW.AyaNova.BLL.User-eventlog": { //handle EventLog entries for users now that we have the user's created //Log //EventLogProcessor.AddEntry(new Event(userId, o.Id, AyaType.User, AyaEvent.Created), ct); //MODIFIED HERE // await ct.SaveChangesAsync(); } break; case "GZTW.AyaNova.BLL.User-locale": { //get the userId var V7Id = new Guid(j["ID"].Value()); var MapItem = importMap.Where(m => m.V7ObjectId == V7Id).FirstOrDefault(); if (MapItem == null) { throw new System.Exception("UserBiz::ImportV7Async-locale - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE USER NOT FOUND IN IMPORTMAP"); } var NewId = MapItem.NewObjectAyaTypeId.ObjectId; User u = ct.User.Where(m => m.Id == NewId).FirstOrDefault(); if (u == null) { throw new System.Exception("UserBiz::ImportV7Async-locale - FATAL ERROR, IMPORT FROM V7 CANNOT CONTINUE USER NOT FOUND IN DATABASE"); } //handle locale entries for users now that we have the locales created var V7Locale = j["DefaultLanguage"].Value(); //Get new locale name var NewLocaleName = string.Empty; switch (V7Locale) { case "Français": NewLocaleName = "fr"; break; case "Español": NewLocaleName = "es"; break; case "Deutsch": NewLocaleName = "de"; break; case "English": NewLocaleName = "en"; break; default: { //It's a custom locale, translate it from v7 original format to imported name format //make lower and replace spaces with dashes NewLocaleName = V7Locale.ToLowerInvariant().Replace(" ", "-"); //ensure each character is a valid path character foreach (char c in System.IO.Path.GetInvalidFileNameChars())//is this kosher on linux? Original code was windows { NewLocaleName = NewLocaleName.Replace(c, '_'); } } break; } u.LocaleId = LocaleBiz.LocaleNameToIdStatic(NewLocaleName, ct); ct.SaveChanges(); } break; case "GZTW.AyaNova.BLL.User-scheduleableusergrouptags": { //handle tag entries for users now that we have the SUG tags created // throw new System.NotImplementedException(); } break; case "GZTW.AyaNova.BLL.User-client": { //handle setting client id for user client login //throw new System.NotImplementedException(); } break; case "GZTW.AyaNova.BLL.User-headoffice": { //handle setting ho id for user headoffice login //throw new System.NotImplementedException(); } break; } //just to hide compiler warning for now await Task.CompletedTask; //this is the equivalent of returning void for a Task signature with nothing to return return true; } //Other job handlers here... ///////////////////////////////////////////////////////////////////// }//eoc }//eons