Files
raven/server/AyaNova/biz/ContractBiz.cs
2023-02-22 23:54:16 +00:00

701 lines
32 KiB
C#

using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace AyaNova.Biz
{/*
Contract general notes / changes
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
Overview
V7 contracts did this:
Limit rates that can be selected to only those selected in contract and which are set to "Contract rate" in themselves
or just offer those contract rates in addition to regular rates
apply a discount to all parts selected for that contract in effect
V8 contracts need to do this:
Markup OR Discount option (mutually exclusive)
Rates in addition to parts
Tag selectable to apply discount or markup
Can have multiple by tag but can have only one by non tag (all parts, all rates)
wo applies discount / markup if part or rate is tagged if tag is chosen
Keep in mind individual Units will be contractable as well
Special notes field kind of like popup notes but it doesn't popup, instead displays on workorder statically somewhere as a reminder.
perhaps a contract in effect section with a ui affordance to open for more details or perhaps not.
I can imagine the first time people would need to read it but after the thousandth it's just taking up space.
Response time duration control for notification purposes, overrides default response time globally set
if a unit has one and customer has one the shortest one applies as this is an entire workorder issue
User can make seperate workorders to work around this if it's an issue
Contracts are never tied directly to an object they affect other than the Customer or Headoffice with a foreign key link
workorders should never be reliant on the existance of a contract but rather just have a contract applied to them for example
Contracts *are* tied to objects that use them like Client, HeadOffice, Unit
Which contract to apply
In order of least to most specific: Headoffice, Customer, Unit
so Unit contract overrides any above contract
Contract applied field
objects with contract that affects then should seperately have a contract name field for contract applied
this supports not directly linking the contract but rather seeing it as an object that is applied to another object,
not linked to it. Can apply, unapply, delete contract and no affect on primary wo or other object
TAGS
in v7 contracts could be set to apply to all parts or none
in v8 need finer control so:
Contract needs selectable tags for any aspect to apply
Discount / markup etc parts / rates
So, in practice like in v7 you set a discount for all parts but then you can also on the same line restrict it to specific part tags and select tags right there
If any items have tags then there can be no "all Items" item? Or do they contradict each other in some way
i.e. a discount for all but a special discount if it's tagged this (could be zero percent so no change if tagged that way as a way to Exclude certain items)
The server then applies the discount based on whether the parts are tagged or not if that's the case in it's bizactions
then the picklist can be supplied the variant for contract id which pulls the contract, finds the tags and then populates the picklist with the extra tag search
MULTIPLE discount / markup ITEMS
Can have ONE default for all box
Can have multiple tagged ones (tag is REQUIRED)
Not two can have same tags in them, that's a biz rule / error
Algorithm:
Default item applied normally UNLESS tagged item which is more specific overrides it
*/
internal class ContractBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, INotifiableObject
{
internal ContractBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = AyaType.Contract;
}
internal static ContractBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new ContractBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new ContractBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Contract.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Contract> CreateAsync(Contract newObject)
{
await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.Contract.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
await SearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await HandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET
//
internal async Task<Contract> GetAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true)
{
var ret = await ct.Contract
.Include(z => z.ContractPartOverrideItems)
.Include(z => z.ContractServiceRateOverrideItems)
.Include(z => z.ContractTravelRateOverrideItems)
.Include(z => z.ServiceRateItems)
.Include(z => z.TravelRateItems)
.AsNoTracking()
.SingleOrDefaultAsync(m => m.Id == id);
if (populateDisplayFields)
await PopulateVizFields(ret);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Contract> PutAsync(Contract putObject)
{
Contract 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(dbObject.Tags);
putObject.CustomFields = JsonUtil.CompactJson(dbObject.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, dbObject.Id, BizType, AyaEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await HandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await PopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
Contract 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.AType == AyaType.Contract && 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.Contract.Remove(dbObject);
await ct.SaveChangesAsync();
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(AyaEvent.Deleted, dbObject);
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task SearchIndexAsync(Contract 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<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id, AyaType specificType)
{
var obj = await GetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(Contract obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Name)
.AddText(obj.Wiki)
.AddText(obj.Tags)
.AddText(obj.AlertNotes)
.AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(Contract proposedObj, Contract currentObj)
{
bool isNew = currentObj == null;
//Name required
if (string.IsNullOrWhiteSpace(proposedObj.Name))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
//If name is otherwise OK, check that name is unique
if (!PropertyHasErrors("Name"))
{
//Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
if (await ct.Contract.AnyAsync(m => m.Name == proposedObj.Name && m.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
}
}
//VALIDATE TAGGED ITEMS
//PARTS
if (proposedObj.ContractPartOverrideItems.Count > 0)
{
//List<string> allTags = new List<string>();
for (int i = 0; i < proposedObj.ContractPartOverrideItems.Count; i++)
{
var item = proposedObj.ContractPartOverrideItems[i];
if (item.Tags.Count < 1)
AddError(ApiErrorCode.VALIDATION_REQUIRED, $"ContractPartOverrideItems[{i}].Tags");
// else
// {
// //add to list, check for dupes
// foreach (string s in item.Tags)
// {
// if (allTags.Contains(s))
// {
// AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, $"ContractPartOverrideItems[{i}].Tags");
// break;
// }
// else
// {
// allTags.Add(s);
// }
// }
// }
if (item.OverridePct == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, $"ContractPartOverrideItems[{i}].OverridePct");
}
}
//SERVICE RATES
if (proposedObj.ContractServiceRateOverrideItems.Count > 0)
{
// List<string> allTags = new List<string>();
//check for overlapping dupes
//[NO DON"T CHECK FOR DUPES, OR AT LEAST NOT LIKE THIS, REMOVING FOR NOW]
//Not sure what the dupe intent was but it should be entirely duped, not just one tag in common as with this
//Check for missing tags
for (int i = 0; i < proposedObj.ContractServiceRateOverrideItems.Count; i++)
{
var item = proposedObj.ContractServiceRateOverrideItems[i];
if (item.Tags.Count < 1)
AddError(ApiErrorCode.VALIDATION_REQUIRED, $"ContractServiceRateOverrideItems[{i}].Tags");
// else
// {
// //add to list, check for dupes
// foreach (string s in item.Tags)
// {
// if (allTags.Contains(s))
// {
// AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, $"ContractServiceRateOverrideItems[{i}].Tags");
// break;
// }
// else
// {
// allTags.Add(s);
// }
// }
// }
if (item.OverridePct == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, $"ContractServiceRateOverrideItems[{i}].OverridePct");
}
}
//TRAVEL RATES
if (proposedObj.ContractTravelRateOverrideItems.Count > 0)
{
for (int i = 0; i < proposedObj.ContractTravelRateOverrideItems.Count; i++)
{
var item = proposedObj.ContractTravelRateOverrideItems[i];
if (item.Tags.Count < 1)
AddError(ApiErrorCode.VALIDATION_REQUIRED, $"ContractTravelRateOverrideItems[{i}].Tags");
// else
// {
// //add to list, check for dupes
// foreach (string s in item.Tags)
// {
// if (allTags.Contains(s))
// {
// AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, $"ContractTravelRateOverrideItems[{i}].Tags");
// break;
// }
// else
// {
// allTags.Add(s);
// }
// }
// }
if (item.OverridePct == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, $"ContractTravelRateOverrideItems[{i}].OverridePct");
}
}
//VALIDATE CONTRACT SERVICE AND TRAVEL RATE ITEMS
//Limit to list requires some to be set
if (proposedObj.ServiceRateItems.Count == 0)
{
if (proposedObj.ContractServiceRatesOnly)
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ContractServiceRatesOnly");
}
//No duplicate rates
//just filter them out rather than setting an error
proposedObj.ServiceRateItems = proposedObj.ServiceRateItems.GroupBy(x => x.ServiceRateId).Select(y => y.First()).ToList();
//Limit to list requires some to be set
if (proposedObj.TravelRateItems.Count == 0)
{
if (proposedObj.ContractTravelRatesOnly)
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ContractTravelRatesOnly");
}
//No duplicate rates
//just filter them out rather than setting an error
proposedObj.TravelRateItems = proposedObj.TravelRateItems.GroupBy(x => x.TravelRateId).Select(y => y.First()).ToList();
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(x => x.FormKey == AyaType.Contract.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(Contract inObj)
{
//FOREIGN KEY CHECKS
if (await ct.Customer.AnyAsync(m => m.ContractId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Customer"));
if (await ct.HeadOffice.AnyAsync(m => m.ContractId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("HeadOffice"));
if (await ct.Unit.AnyAsync(m => m.ContractId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Unit"));
if (await ct.WorkOrder.AnyAsync(m => m.ContractId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("WorkOrder"));
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> 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.Contract.Include(z => z.ContractPartOverrideItems)
.Include(z => z.ContractServiceRateOverrideItems)
.Include(z => z.ContractTravelRateOverrideItems)
.Include(z => z.ServiceRateItems)
.Include(z => z.TravelRateItems)
.AsNoTracking()
.Where(z => batch.Contains(z.Id))
.ToArrayAsync();
//order the results back into original
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
batchResults = null;
foreach (Contract 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(Contract o)
{
if (contractOverrideTypeEnumList == null)
contractOverrideTypeEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList(
StringUtil.TrimTypeName(typeof(ContractOverrideType).ToString()),
UserTranslationId,
CurrentUserRoles);
if (preTrans == null)
preTrans = await TranslationBiz.GetSubsetStaticAsync(
new List<string> { "TimeSpanDays", "TimeSpanHours", "TimeSpanMinutes", "TimeSpanSeconds" },
UserTranslationId);
if (o.ResponseTime == TimeSpan.Zero)
o.ResponseTimeViz = string.Empty;
else
o.ResponseTimeViz = $"{(preTrans["TimeSpanDays"])}: {o.ResponseTime.Days}, {(preTrans["TimeSpanHours"])}: {o.ResponseTime.Hours}, {(preTrans["TimeSpanMinutes"])}: {o.ResponseTime.Minutes} ";
o.PartsOverrideTypeViz = contractOverrideTypeEnumList.Where(x => x.Id == (long)o.PartsOverrideType).Select(x => x.Name).First();
o.TravelRatesOverrideTypeViz = contractOverrideTypeEnumList.Where(x => x.Id == (long)o.TravelRatesOverrideType).Select(x => x.Name).First();
o.ServiceRatesOverrideTypeViz = contractOverrideTypeEnumList.Where(x => x.Id == (long)o.ServiceRatesOverrideType).Select(x => x.Name).First();
foreach (var i in o.ContractPartOverrideItems)
i.OverrideTypeViz = contractOverrideTypeEnumList.Where(x => x.Id == (long)i.OverrideType).Select(x => x.Name).First();
foreach (var i in o.ContractTravelRateOverrideItems)
i.OverrideTypeViz = contractOverrideTypeEnumList.Where(x => x.Id == (long)i.OverrideType).Select(x => x.Name).First();
foreach (var i in o.ContractServiceRateOverrideItems)
i.OverrideTypeViz = contractOverrideTypeEnumList.Where(x => x.Id == (long)i.OverrideType).Select(x => x.Name).First();
foreach (var i in o.ServiceRateItems)
{
if (!vc.Has("servicerate", i.ServiceRateId))
{
vc.Add(await ct.ServiceRate.AsNoTracking().Where(x => x.Id == i.ServiceRateId).Select(x => x.Name).FirstOrDefaultAsync(), "servicerate", i.ServiceRateId);
}
i.ServiceRateViz = vc.Get("servicerate", i.ServiceRateId);
}
foreach (var i in o.TravelRateItems)
{
if (!vc.Has("travelrate", i.TravelRateId))
{
vc.Add(await ct.TravelRate.AsNoTracking().Where(x => x.Id == i.TravelRateId).Select(x => x.Name).FirstOrDefaultAsync(), "travelrate", i.TravelRateId);
}
i.TravelRateViz = vc.Get("travelrate", i.TravelRateId);
}
}
private List<NameIdItem> contractOverrideTypeEnumList = null;
private Dictionary<string, string> preTrans = null;
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//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, 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($"CustomerBiz.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<long> idList = new List<long>();
long FailedObjectCount = 0;
JObject jobData = JObject.Parse(job.JobInfo);
if (jobData.ContainsKey("idList"))
idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
else
idList = await ct.Contract.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();
Contract 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(AyaNova.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(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<CustomerBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons