diff --git a/server/biz/GZCaseBiz.cs b/server/biz/GZCaseBiz.cs new file mode 100644 index 0000000..b0da4f8 --- /dev/null +++ b/server/biz/GZCaseBiz.cs @@ -0,0 +1,461 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System.Linq; +using Sockeye.Util; +using Sockeye.Api.ControllerHelpers; +using Sockeye.Models; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Sockeye.Biz +{ + internal class GZCaseBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, INotifiableObject + { + internal GZCaseBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserTranslationId = userTranslationId; + CurrentUserRoles = UserRoles; + BizType = SockType.GZCase; + } + + internal static GZCaseBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + { + if (httpContext != null) + return new GZCaseBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + else + return new GZCaseBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //EXISTS + internal async Task ExistsAsync(long id) + { + return await ct.GZCase.AnyAsync(z => z.Id == id); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //CREATE + // + internal async Task CreateAsync(GZCase newObject) + { + await ValidateAsync(newObject, null); + if (HasErrors) + return null; + else + { + newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); + newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); + await ct.GZCase.AddAsync(newObject); + await ct.SaveChangesAsync(); + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct); + await SearchIndexAsync(newObject, true); + await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); + await HandlePotentialNotificationEvent(SockEvent.Created, newObject); + return newObject; + } + } + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //GET + // + internal async Task GetAsync(long id, bool logTheGetEvent = true) + { + var ret = await ct.GZCase.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); + if (logTheGetEvent && ret != null) + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct); + return ret; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //UPDATE + // + internal async Task PutAsync(GZCase putObject) + { + var dbObject = await GetAsync(putObject.Id, false); + 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 ValidateAsync(putObject, dbObject); + if (HasErrors) return null; + ct.Replace(dbObject, putObject); + try + { + await ct.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!await ExistsAsync(putObject.Id)) + AddError(ApiErrorCode.NOT_FOUND); + else + AddError(ApiErrorCode.CONCURRENCY_CONFLICT); + return null; + } + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, BizType, SockEvent.Modified), ct); + await SearchIndexAsync(putObject, false); + await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); + await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject); + return putObject; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DELETE + // + internal async Task DeleteAsync(long id) + { + using (var transaction = await ct.Database.BeginTransactionAsync()) + { + + GZCase dbObject = await GetAsync(id, false); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND); + return false; + } + await ValidateCanDeleteAsync(dbObject); + if (HasErrors) + return false; + + + + + { + var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.GZCase && x.ObjectId == id).Select(x => x.Id).ToListAsync(); + if (IDList.Count() > 0) + { + ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles); + foreach (long ItemId in IDList) + if (!await b.DeleteAsync(ItemId, transaction)) + { + AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}"); + return false; + } + } + } + + ct.GZCase.Remove(dbObject); + await ct.SaveChangesAsync(); + + //Log event + await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct); + await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); + await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); + await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); + await transaction.CommitAsync(); + await HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject); + + return true; + } + } + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //SEARCH + // + private async Task SearchIndexAsync(GZCase 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, SockType specificType) + { + var obj = await GetAsync(id, false); + var SearchParams = new Search.SearchIndexProcessObjectParameters(); + DigestSearchText(obj, SearchParams); + return SearchParams; + } + + public void DigestSearchText(GZCase obj, Search.SearchIndexProcessObjectParameters searchParams) + { + if (obj != null) + searchParams.AddText(obj.DbId) + .AddText(obj.FetchCode) + .AddText(obj.Wiki) + .AddText(obj.Tags) + .AddText(obj.FetchEmail) + .AddText(obj.Key) + .AddText(obj.RegTo) + .AddCustomFields(obj.CustomFields); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + + private async Task ValidateAsync(GZCase proposedObj, GZCase currentObj) + { + bool isNew = currentObj == null; + + + + //Any form customizations to validate? + var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == SockType.GZCase.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 async Task ValidateCanDeleteAsync(GZCase inObj) + { + + await Task.CompletedTask; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //REPORTING + // + public async Task GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId) + { + 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(); + + //query for this batch, comes back in db natural order unfortunately + var batchResults = await ct.GZCase.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync(); + + //order the results back into original + //What is happening here: + //for performance the query is batching a bunch at once by fetching a block of items from the sql server + //however it's returning in db order which is often not the order the id list is in + //so it needs to be sorted back into the same order as the ide list + //This would not be necessary if just fetching each one at a time individually (like in workorder get report data) + + var orderedList = from id in batch join z in batchResults on id equals z.Id select z; + batchResults = null; + + foreach (GZCase w in orderedList) + { + if (!ReportRenderManager.KeepGoing(jobId)) return null; + await PopulateVizFields(w); + var jo = JObject.FromObject(w); + if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"])) + jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]); + ReportData.Add(jo); + } + orderedList = null; + } + vc.Clear(); + return ReportData; + } + private VizCache vc = new VizCache(); + + + //populate viz fields from provided object + private async Task PopulateVizFields(GZCase o) + { + if (!vc.Has("customer", o.CustomerId)) + { + vc.Add(await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => x.Name).FirstOrDefaultAsync(), "customer", o.CustomerId); + } + o.CustomerViz = vc.Get("customer", o.CustomerId); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // IMPORT EXPORT + // + + public async Task GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId) + { + return await GetReportData(dataListSelectedRequest, jobId); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //JOB / OPERATIONS + // + public async Task HandleJobAsync(OpsJob job) + { + //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) + { + case JobType.BatchCoreObjectOperation: + await ProcessBatchJobAsync(job); + break; + default: + throw new System.ArgumentOutOfRangeException($"GZCaseBiz.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.GZCase.AsNoTracking().Select(z => z.Id).ToListAsync(); + bool SaveIt = false; + + //--------------------------------- + //case 4192 + TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS); + DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1)); + var TotalRecords = idList.LongCount(); + long CurrentRecord = -1; + //--------------------------------- + + foreach (long id in idList) + { + try + { + //-------------------------------- + //case 4192 + //Update progress / cancel requested? + CurrentRecord++; + if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan)) + { + await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}"); + if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested) + break; + LastProgressCheck = DateTime.UtcNow; + } + //--------------------------------- + + SaveIt = false; + ClearErrors(); + GZCase o = null; + //save a fetch if it's a delete + if (job.SubType != JobSubType.Delete) + o = await GetAsync(id, false); + 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 DeleteAsync(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 PutAsync(o); + if (o == null) + { + await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}"); + FailedObjectCount++; + } + } + + //delay so we're not tying up all the resources in a tight loop + await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY); + } + catch (Exception ex) + { + await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})"); + await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex)); + } + } + + //--------------------------------- + //case 4192 + await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}"); + //--------------------------------- + + 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 HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) + { + ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger(); + + log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, AyaEvent:{ayaEvent}]"); + + bool isNew = currentObj == null; + + proposedObj.Name = "LICENSE ID" + proposedObj.Id.ToString(); + //STANDARD EVENTS FOR ALL OBJECTS + await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); + + //SPECIFIC EVENTS FOR THIS OBJECT + GZCase o = (GZCase)proposedObj; + + //## DELETED EVENTS + //any event added below needs to be removed, so + //just blanket remove any event for this object of eventtype that would be added below here + //do it regardless any time there's an update and then + //let this code below handle the refreshing addition that could have changes + //await NotifyEventHelper.ClearPriorEventsForObject(ct, SockType.GZCase, o.Id, NotifyEventType.ContractExpiring); + + + //## CREATED / MODIFIED EVENTS + if (ayaEvent == SockEvent.Created || ayaEvent == SockEvent.Modified) + { + + + + } + + }//end of process notifications + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/biz/LicenseBiz.cs b/server/biz/LicenseBiz.cs new file mode 100644 index 0000000..b900392 --- /dev/null +++ b/server/biz/LicenseBiz.cs @@ -0,0 +1,461 @@ +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System.Linq; +using Sockeye.Util; +using Sockeye.Api.ControllerHelpers; +using Sockeye.Models; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Sockeye.Biz +{ + internal class LicenseBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, INotifiableObject + { + internal LicenseBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) + { + ct = dbcontext; + UserId = currentUserId; + UserTranslationId = userTranslationId; + CurrentUserRoles = UserRoles; + BizType = SockType.License; + } + + internal static LicenseBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) + { + if (httpContext != null) + return new LicenseBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); + else + return new LicenseBiz(ct, 1, ServerBootConfig.SOCKEYE_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //EXISTS + internal async Task ExistsAsync(long id) + { + return await ct.License.AnyAsync(z => z.Id == id); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //CREATE + // + internal async Task CreateAsync(License newObject) + { + await ValidateAsync(newObject, null); + if (HasErrors) + return null; + else + { + newObject.Tags = TagBiz.NormalizeTags(newObject.Tags); + newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields); + await ct.License.AddAsync(newObject); + await ct.SaveChangesAsync(); + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, SockEvent.Created), ct); + await SearchIndexAsync(newObject, true); + await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null); + await HandlePotentialNotificationEvent(SockEvent.Created, newObject); + return newObject; + } + } + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //GET + // + internal async Task GetAsync(long id, bool logTheGetEvent = true) + { + var ret = await ct.License.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id); + if (logTheGetEvent && ret != null) + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, SockEvent.Retrieved), ct); + return ret; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //UPDATE + // + internal async Task PutAsync(License putObject) + { + var dbObject = await GetAsync(putObject.Id, false); + 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 ValidateAsync(putObject, dbObject); + if (HasErrors) return null; + ct.Replace(dbObject, putObject); + try + { + await ct.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!await ExistsAsync(putObject.Id)) + AddError(ApiErrorCode.NOT_FOUND); + else + AddError(ApiErrorCode.CONCURRENCY_CONFLICT); + return null; + } + await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, BizType, SockEvent.Modified), ct); + await SearchIndexAsync(putObject, false); + await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags); + await HandlePotentialNotificationEvent(SockEvent.Modified, putObject, dbObject); + return putObject; + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //DELETE + // + internal async Task DeleteAsync(long id) + { + using (var transaction = await ct.Database.BeginTransactionAsync()) + { + + License dbObject = await GetAsync(id, false); + if (dbObject == null) + { + AddError(ApiErrorCode.NOT_FOUND); + return false; + } + await ValidateCanDeleteAsync(dbObject); + if (HasErrors) + return false; + + + + + { + var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.License && x.ObjectId == id).Select(x => x.Id).ToListAsync(); + if (IDList.Count() > 0) + { + ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles); + foreach (long ItemId in IDList) + if (!await b.DeleteAsync(ItemId, transaction)) + { + AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}"); + return false; + } + } + } + + ct.License.Remove(dbObject); + await ct.SaveChangesAsync(); + + //Log event + await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Name, ct); + await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, BizType, ct); + await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags); + await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct); + await transaction.CommitAsync(); + await HandlePotentialNotificationEvent(SockEvent.Deleted, dbObject); + + return true; + } + } + + + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //SEARCH + // + private async Task SearchIndexAsync(License 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, SockType specificType) + { + var obj = await GetAsync(id, false); + var SearchParams = new Search.SearchIndexProcessObjectParameters(); + DigestSearchText(obj, SearchParams); + return SearchParams; + } + + public void DigestSearchText(License obj, Search.SearchIndexProcessObjectParameters searchParams) + { + if (obj != null) + searchParams.AddText(obj.DbId) + .AddText(obj.FetchCode) + .AddText(obj.Wiki) + .AddText(obj.Tags) + .AddText(obj.FetchEmail) + .AddText(obj.Key) + .AddText(obj.RegTo) + .AddCustomFields(obj.CustomFields); + } + + //////////////////////////////////////////////////////////////////////////////////////////////// + //VALIDATION + // + + private async Task ValidateAsync(License proposedObj, License currentObj) + { + bool isNew = currentObj == null; + + + + //Any form customizations to validate? + var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == SockType.License.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 async Task ValidateCanDeleteAsync(License inObj) + { + + await Task.CompletedTask; + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //REPORTING + // + public async Task GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId) + { + 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(); + + //query for this batch, comes back in db natural order unfortunately + var batchResults = await ct.License.AsNoTracking().Where(z => batch.Contains(z.Id)).ToArrayAsync(); + + //order the results back into original + //What is happening here: + //for performance the query is batching a bunch at once by fetching a block of items from the sql server + //however it's returning in db order which is often not the order the id list is in + //so it needs to be sorted back into the same order as the ide list + //This would not be necessary if just fetching each one at a time individually (like in workorder get report data) + + var orderedList = from id in batch join z in batchResults on id equals z.Id select z; + batchResults = null; + + foreach (License w in orderedList) + { + if (!ReportRenderManager.KeepGoing(jobId)) return null; + await PopulateVizFields(w); + var jo = JObject.FromObject(w); + if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"])) + jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]); + ReportData.Add(jo); + } + orderedList = null; + } + vc.Clear(); + return ReportData; + } + private VizCache vc = new VizCache(); + + + //populate viz fields from provided object + private async Task PopulateVizFields(License o) + { + if (!vc.Has("customer", o.CustomerId)) + { + vc.Add(await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => x.Name).FirstOrDefaultAsync(), "customer", o.CustomerId); + } + o.CustomerViz = vc.Get("customer", o.CustomerId); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + // IMPORT EXPORT + // + + public async Task GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId) + { + return await GetReportData(dataListSelectedRequest, jobId); + } + + + //////////////////////////////////////////////////////////////////////////////////////////////// + //JOB / OPERATIONS + // + public async Task HandleJobAsync(OpsJob job) + { + //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) + { + case JobType.BatchCoreObjectOperation: + await ProcessBatchJobAsync(job); + break; + default: + throw new System.ArgumentOutOfRangeException($"LicenseBiz.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.License.AsNoTracking().Select(z => z.Id).ToListAsync(); + bool SaveIt = false; + + //--------------------------------- + //case 4192 + TimeSpan ProgressAndCancelCheckSpan = new TimeSpan(0, 0, ServerBootConfig.JOB_PROGRESS_UPDATE_AND_CANCEL_CHECK_SECONDS); + DateTime LastProgressCheck = DateTime.UtcNow.Subtract(new TimeSpan(1, 1, 1, 1, 1)); + var TotalRecords = idList.LongCount(); + long CurrentRecord = -1; + //--------------------------------- + + foreach (long id in idList) + { + try + { + //-------------------------------- + //case 4192 + //Update progress / cancel requested? + CurrentRecord++; + if (DateUtil.IsAfterDuration(LastProgressCheck, ProgressAndCancelCheckSpan)) + { + await JobsBiz.UpdateJobProgressAsync(job.GId, $"{CurrentRecord}/{TotalRecords}"); + if (await JobsBiz.GetJobStatusAsync(job.GId) == JobStatus.CancelRequested) + break; + LastProgressCheck = DateTime.UtcNow; + } + //--------------------------------- + + SaveIt = false; + ClearErrors(); + License o = null; + //save a fetch if it's a delete + if (job.SubType != JobSubType.Delete) + o = await GetAsync(id, false); + 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 DeleteAsync(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 PutAsync(o); + if (o == null) + { + await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}"); + FailedObjectCount++; + } + } + + //delay so we're not tying up all the resources in a tight loop + await Task.Delay(Sockeye.Util.ServerBootConfig.JOB_OBJECT_HANDLE_BATCH_JOB_LOOP_DELAY); + } + catch (Exception ex) + { + await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})"); + await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex)); + } + } + + //--------------------------------- + //case 4192 + await JobsBiz.UpdateJobProgressAsync(job.GId, $"{++CurrentRecord}/{TotalRecords}"); + //--------------------------------- + + 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 HandlePotentialNotificationEvent(SockEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) + { + ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger(); + + log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, AyaEvent:{ayaEvent}]"); + + bool isNew = currentObj == null; + + proposedObj.Name = "LICENSE ID" + proposedObj.Id.ToString(); + //STANDARD EVENTS FOR ALL OBJECTS + await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct); + + //SPECIFIC EVENTS FOR THIS OBJECT + License o = (License)proposedObj; + + //## DELETED EVENTS + //any event added below needs to be removed, so + //just blanket remove any event for this object of eventtype that would be added below here + //do it regardless any time there's an update and then + //let this code below handle the refreshing addition that could have changes + //await NotifyEventHelper.ClearPriorEventsForObject(ct, SockType.License, o.Id, NotifyEventType.ContractExpiring); + + + //## CREATED / MODIFIED EVENTS + if (ayaEvent == SockEvent.Created || ayaEvent == SockEvent.Modified) + { + + + + } + + }//end of process notifications + + + ///////////////////////////////////////////////////////////////////// + + }//eoc + + +}//eons + diff --git a/server/biz/SockType.cs b/server/biz/SockType.cs index 93ad0bf..61c6c94 100644 --- a/server/biz/SockType.cs +++ b/server/biz/SockType.cs @@ -68,12 +68,14 @@ namespace Sockeye.Biz License = 93, [CoreBizObject, ReportableBizObject] TrialLicenseRequest = 94, - [CoreBizObject, ReportableBizObject] + [CoreBizObject, ReportableBizObject] SubscriptionServer = 95, [CoreBizObject, ReportableBizObject] Purchase = 96, [CoreBizObject, ReportableBizObject] - Product = 97 + Product = 97, + [CoreBizObject, ReportableBizObject] + GZCase = 98 diff --git a/server/biz/VendorBiz.cs b/server/biz/VendorBiz.cs index 03dc88b..6863b18 100644 --- a/server/biz/VendorBiz.cs +++ b/server/biz/VendorBiz.cs @@ -162,7 +162,7 @@ namespace Sockeye.Biz if (HasErrors) return false; { - var IDList = await ct.Review.AsNoTracking().Where(x => x.AType == SockType.Vendor && x.ObjectId == id).Select(x => x.Id).ToListAsync(); + var IDList = await ct.Review.AsNoTracking().Where(x => x.SockType == SockType.Vendor && x.ObjectId == id).Select(x => x.Id).ToListAsync(); if (IDList.Count() > 0) { ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles); @@ -257,8 +257,6 @@ namespace Sockeye.Biz private async Task ValidateAsync(Vendor proposedObj, Vendor currentObj) { - //skip validation if seeding - if (ServerBootConfig.SEEDING) return; bool isNew = currentObj == null; @@ -300,22 +298,12 @@ namespace Sockeye.Biz //FOREIGN KEY CHECKS if (await ct.User.AnyAsync(m => m.VendorId == inObj.Id)) AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User")); - if (await ct.UnitModel.AnyAsync(m => m.VendorId == inObj.Id)) - AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("UnitModel")); - if (await ct.Unit.AnyAsync(m => m.PurchasedFromVendorId == inObj.Id)) - AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Unit")); + if (await ct.Product.AnyAsync(m => m.VendorId == inObj.Id)) + AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Product")); + if (await ct.Purchase.AnyAsync(m => m.VendorId == inObj.Id)) + AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Purchase")); - //part has three potential references, if any match that's good enough since it's the same message - if (await ct.Part.AnyAsync(z => z.ManufacturerId == inObj.Id) == true) - AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Part")); - else if (await ct.Part.AnyAsync(z => z.WholeSalerId == inObj.Id) == true) - AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Part")); - else if (await ct.Part.AnyAsync(z => z.AlternativeWholeSalerId == inObj.Id) == true) - AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Part")); - - if (await ct.PurchaseOrder.AnyAsync(m => m.VendorId == inObj.Id)) - AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PurchaseOrder")); } @@ -551,7 +539,7 @@ namespace Sockeye.Biz public async Task HandlePotentialNotificationEvent(SockEvent SockEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = Sockeye.Util.ApplicationLogging.CreateLogger(); - if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; + log.LogDebug($"HandlePotentialNotificationEvent processing: [SockType:{this.BizType}, SockEvent:{SockEvent}]"); bool isNew = currentObj == null; diff --git a/server/models/AyContext.cs b/server/models/AyContext.cs index f3ea603..66ec7b8 100644 --- a/server/models/AyContext.cs +++ b/server/models/AyContext.cs @@ -44,7 +44,7 @@ namespace Sockeye.Models public virtual DbSet Logo { get; set; } public virtual DbSet Report { get; set; } public virtual DbSet DashboardView { get; set; } - + public virtual DbSet CustomerNotifySubscription { get; set; } public virtual DbSet CustomerNotifyEvent { get; set; } @@ -53,10 +53,15 @@ namespace Sockeye.Models public virtual DbSet Integration { get; set; } - public virtual DbSet IntegrationItem { get; set; } + public virtual DbSet IntegrationItem { get; set; } public virtual DbSet IntegrationLog { get; set; } - public virtual DbSet License { get; set; } + public virtual DbSet License { get; set; } + public virtual DbSet Vendor { get; set; } + public virtual DbSet Product { get; set; } + public virtual DbSet Purchase { get; set; } + public virtual DbSet TrialLicenseRequest { get; set; } + public virtual DbSet GZCase { get; set; } //Note: had to add this constructor to work with the code in startup.cs that gets the connection string from the appsettings.json file //and commented out the above on configuring @@ -115,8 +120,8 @@ namespace Sockeye.Models /////////////////////////////// //SERIALIZED OBJECTS // - - modelBuilder.Entity().Property(z => z.CaseId).UseIdentityByDefaultColumn(); + + modelBuilder.Entity().Property(z => z.CaseId).UseIdentityByDefaultColumn(); //## NOTE: if more added here then must also update globalbizsettingscontroller.seeds and client ////////////////////////////////////////////////////////////// diff --git a/server/models/GZCase.cs b/server/models/GZCase.cs new file mode 100644 index 0000000..3352647 --- /dev/null +++ b/server/models/GZCase.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using Sockeye.Biz; + +namespace Sockeye.Models +{ + public class GZCase : ICoreBizObjectModel + { + public long Id { get; set; } + public uint Concurrency { get; set; } + + public long CaseId { get; set; } + public DateTime Created { get; set; } + public DateTime? Closed { get; set; } + + [Required] + public string Name { get; set; } + public string Notes { get; set; } + public string Wiki { get; set; } + public string CustomFields { get; set; } + public List Tags { get; set; } + + public GZCase() + { + Tags = new List(); + Created = DateTime.UtcNow; + } + [NotMapped, JsonIgnore] + public SockType SType { get => SockType.GZCase; } + }//eoc +}//eons diff --git a/server/models/License.cs b/server/models/License.cs index 5e8d13e..0110d38 100644 --- a/server/models/License.cs +++ b/server/models/License.cs @@ -32,6 +32,9 @@ namespace Sockeye.Models public string CustomFields { get; set; } public List Tags { get; set; } + //workaround for notification + [NotMapped, JsonIgnore] + public string Name { get; set; } public License() { diff --git a/server/models/Product.cs b/server/models/Product.cs new file mode 100644 index 0000000..3d5295e --- /dev/null +++ b/server/models/Product.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using Sockeye.Biz; + +namespace Sockeye.Models +{ + public class Product : ICoreBizObjectModel + { + public long Id { get; set; } + public uint Concurrency { get; set; } + + [Required] + public string Name { get; set; } + public bool Active { get; set; } + public long VendorId { get; set; } + public TimeSpan LicenseInterval { get; set; } + public TimeSpan MaintInterval { get; set; } + public string VendorCode { get; set; } + public string OurCode { get; set; } + public string Wiki { get; set; } + public string CustomFields { get; set; } + public List Tags { get; set; } + + public Product() + { + Tags = new List(); + } + + [NotMapped, JsonIgnore] + public SockType SType { get => SockType.Product; } + }//eoc +}//eons diff --git a/server/models/Purchase.cs b/server/models/Purchase.cs new file mode 100644 index 0000000..9028bdb --- /dev/null +++ b/server/models/Purchase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using Sockeye.Biz; + +namespace Sockeye.Models +{ + public class Purchase : ICoreBizObjectModel + { + public long Id { get; set; } + public uint Concurrency { get; set; } + [Required] + public long CustomerId { get; set; } + [Required] + public long VendorId { get; set; } + [Required] + public long ProductId { get; set; } + public string SalesOrderNumber { get; set; } + [Required] + public DateTime PurchaseDate { get; set; } + public DateTime? ExpireDate { get; set; } + public DateTime? CancelDate { get; set; } + public string CouponCode { get; set; } + public string PurchaseNotes { get; set; } + public bool RenewNoticeSent { get; set; } = false; + public int Quantity { get; set; } = 1; + public string VendorData { get; set; } + public DateTime? ProcessedDate { get; set; } + public string Wiki { get; set; } + public string CustomFields { get; set; } + public List Tags { get; set; } + + public Purchase() + { + Tags = new List(); + } + + [NotMapped, JsonIgnore] + public SockType SType { get => SockType.Purchase; } + }//eoc +}//eons diff --git a/server/models/SubscriptionServer.cs b/server/models/SubscriptionServer.cs new file mode 100644 index 0000000..55e6576 --- /dev/null +++ b/server/models/SubscriptionServer.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Newtonsoft.Json; +using Sockeye.Biz; + +namespace Sockeye.Models +{ + public class SubscriptionServer : ICoreBizObjectModel + { + public long Id { get; set; } + public uint Concurrency { get; set; } + + [Required] + public string Name { get; set; } + public bool Active { get; set; } + public DateTime Created { get; set; } + [Required] + public string DataCenter { get; set; } + [Required] + public string TimeZone { get; set; } + public DateTime? LastUpdated { get; set; } + public DateTime SubscriptionExpire { get; set; } + public DateTime? TrialExpire { get; set; } + public string TrialContact { get; set; } + public string TrialEmail { get; set; } + public string TrialCompany { get; set; } + public string OperatingSystem { get; set; } + public string CustomerSubDomain { get; set; } + public long? CustomerId { get; set; } + public long? LicenseId { get; set; } + public string Wiki { get; set; } + public string CustomFields { get; set; } + public List Tags { get; set; } + + + + public SubscriptionServer() + { + Tags = new List(); + Created = DateTime.UtcNow; + } + + [NotMapped, JsonIgnore] + public SockType SType { get => SockType.SubscriptionServer; } + + }//eoc + +}//eons diff --git a/server/models/TrialLicenseRequest.cs b/server/models/TrialLicenseRequest.cs index 92b0450..021d30c 100644 --- a/server/models/TrialLicenseRequest.cs +++ b/server/models/TrialLicenseRequest.cs @@ -28,6 +28,14 @@ namespace Sockeye.Models public DateTime FetchedOn { get; set; } public bool Perpetual { get; set; } = false; + + public List Tags { get; set; } + + //workaround for notification + [NotMapped, JsonIgnore] + public string Name { get; set; } + + public TrialLicenseRequest() { diff --git a/server/util/AySchema.cs b/server/util/AySchema.cs index cc98b86..f07c6c6 100644 --- a/server/util/AySchema.cs +++ b/server/util/AySchema.cs @@ -711,6 +711,7 @@ BEGIN when 95 then aytable = 'asubscriptionserver'; when 96 then aytable = 'apurchase'; when 97 then aytable = 'aproduct'; + when 98 then aytable = 'agzcase'; else RETURN returnstr; @@ -886,8 +887,8 @@ $BODY$ LANGUAGE PLPGSQL STABLE"); await ExecQueryAsync("CREATE TABLE asubscriptionserver (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, active BOOL NOT NULL DEFAULT true, created TIMESTAMPTZ NOT NULL, " - + "datacenter TEXT NOT NULL, timezone TEXT NOT NULL, lastupdated TIMESTAMPTZ, subscriptionexpire TIMESTAMPTZ NOT NULL, " - + "trialexpire TIMESTAMPTZ, trialcontact TEXT, trialemail TEXT, trialcompany TEXT, operatingsystem TEXT,ourdomain TEXT, customsubdomain TEXT, " + + "name TEXT NOT NULL, datacenter TEXT NOT NULL, timezone TEXT NOT NULL, lastupdated TIMESTAMPTZ, subscriptionexpire TIMESTAMPTZ NOT NULL, " + + "trialexpire TIMESTAMPTZ, trialcontact TEXT, trialemail TEXT, trialcompany TEXT, operatingsystem TEXT, customersubdomain TEXT, " + "wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY, licenseid BIGINT REFERENCES alicense(id), customerid BIGINT REFERENCES acustomer(id) )"); @@ -904,8 +905,8 @@ $BODY$ LANGUAGE PLPGSQL STABLE"); + "wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY )"); - await ExecQueryAsync("CREATE TABLE acase (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, caseid BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, " - + "created TIMESTAMPTZ NOT NULL, closed TIMESTAMPTZ, title TEXT NOT NULL, notes TEXT, " + await ExecQueryAsync("CREATE TABLE agzcase (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, caseid BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL, " + + "created TIMESTAMPTZ NOT NULL, closed TIMESTAMPTZ, name TEXT NOT NULL, notes TEXT, " + "wiki TEXT, customfields TEXT, tags VARCHAR(255) ARRAY )");