Files
raven/server/AyaNova/biz/UnitBiz.cs
2021-09-30 20:24:48 +00:00

615 lines
28 KiB
C#

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Linq;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace AyaNova.Biz
{
internal class UnitBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject, IImportAbleObject, INotifiableObject
{
internal UnitBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = AyaType.Unit;
}
internal static UnitBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new UnitBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new UnitBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExistsAsync(long id)
{
return await ct.Unit.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Unit> CreateAsync(Unit newObject)
{
await ValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.Unit.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<Unit> GetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.Unit.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Unit> PutAsync(Unit 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, dbObject.Id, BizType, AyaEvent.Modified), ct);
await SearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await HandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> DeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
Unit dbObject = await GetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
await ValidateCanDeleteAsync(dbObject);
if (HasErrors)
return false;
if (HasErrors)
return false;
ct.Unit.Remove(dbObject);
await ct.SaveChangesAsync();
await EventLogProcessor.DeleteObjectLogAsync(UserId, BizType, dbObject.Id, dbObject.Serial, 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(Unit 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(Unit obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Serial)
.AddText(obj.Wiki)
.AddText(obj.Tags)
.AddText(obj.Receipt)
.AddText(obj.Description)
.AddText(obj.WarrantyTerms)
.AddText(obj.Text1)
.AddText(obj.Text2)
.AddText(obj.Text3)
.AddText(obj.Text4)
.AddText(obj.Address)
.AddText(obj.City)
.AddText(obj.Region)
.AddText(obj.Country)
.AddText(obj.Latitude)
.AddText(obj.Longitude)
.AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ValidateAsync(Unit proposedObj, Unit currentObj)
{
bool isNew = currentObj == null;
//Serial required
if (string.IsNullOrWhiteSpace(proposedObj.Serial))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Serial");
//If serial is otherwise OK, check that serial is unique for that unitmodelid (this is to catch dupes)
//(two different manufacturers products could have the same serial easily, but it's less likely for two different units of the same unitmodel)
//
if (!PropertyHasErrors("Serial"))
{
//Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
if (await ct.Unit.AnyAsync(z => z.Serial == proposedObj.Serial && z.UnitModelId == proposedObj.UnitModelId && z.Id != proposedObj.Id))
{
AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Serial", "no two units can have the same serial and same unitmodel");
}
}
if (proposedObj.ContractId != null)
if (proposedObj.ContractExpires == null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "ContractExpires");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Unit.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(Unit inObj)
{
//FOREIGN KEY CHECKS
if (await ct.Unit.AnyAsync(m => m.ParentUnitId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("UnitParentUnitID"));
if (await ct.Unit.AnyAsync(m => m.ReplacedByUnitId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("UnitReplacedByUnitID"));
if (await ct.LoanUnit.AnyAsync(z => z.UnitId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("LoanUnit"));
if (await ct.CustomerServiceRequest.AnyAsync(z => z.UnitId == inObj.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("CustomerServiceRequest"));
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest)
{
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.Unit.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;
foreach (Unit w in orderedList)
{
await PopulateVizFields(w);
var jo = JObject.FromObject(w);
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
}
return ReportData;
}
//populate viz fields from provided object
private async Task PopulateVizFields(Unit o)
{
o.CustomerViz = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.UnitModelId != null)
o.UnitModelViz = await ct.UnitModel.AsNoTracking().Where(x => x.Id == o.UnitModelId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.ParentUnitId != null)
o.ParentUnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.ParentUnitId).Select(x => x.Serial).FirstOrDefaultAsync();
if (o.ReplacedByUnitId != null)
o.ReplacedByUnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.ReplacedByUnitId).Select(x => x.Serial).FirstOrDefaultAsync();
if (o.PurchasedFromVendorId != null)
o.PurchasedFromVendorViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.PurchasedFromVendorId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.ContractId != null)
o.ContractViz = await ct.Contract.AsNoTracking().Where(x => x.Id == o.ContractId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.Metered)
{
var lastMeter = await ct.UnitMeterReading.AsNoTracking().OrderByDescending(m => m.MeterDate).FirstOrDefaultAsync(x => x.UnitId == o.Id);
if (lastMeter != null)
{
o.LastMeterViz = lastMeter.Meter;
o.LastMeterDateViz = lastMeter.MeterDate;
o.LastMeterNotesViz = lastMeter.Notes;
}
}
/*
last completed work order stuff, way to complex to fetch via EF Core so dropping to raw sql
SELECT serial AS LASTWORKORDERSERIAL,
SERVICEDATE AS LASTWORKORDERSERVICEDATE,
AWORKORDERITEMUNIT.UNITID AS LASTWORKORDERUNITID,
AWORKORDERITEMUNIT.ID AS LASTWORKORDERITEMUNITID
FROM AWORKORDER
LEFT JOIN AWORKORDERITEM ON AWORKORDER.ID = AWORKORDERITEM.WORKORDERID
LEFT JOIN AWORKORDERSTATUS ON AWORKORDER.LASTSTATUSID = AWORKORDERSTATUS.ID
LEFT JOIN AWORKORDERITEMUNIT ON AWORKORDERITEM.ID = AWORKORDERITEMUNIT.WORKORDERITEMID
WHERE AWORKORDERITEMUNIT.ID = AMAINUNIT.ID
AND AWORKORDERSTATUS.COMPLETED = TRUE
ORDER BY AWORKORDER.ID DESC
LIMIT 1
*/
using (var command = ct.Database.GetDbConnection().CreateCommand())
{
await ct.Database.OpenConnectionAsync();
command.CommandText = @$"SELECT serial AS LASTWORKORDERSERIAL,
SERVICEDATE AS LASTWORKORDERSERVICEDATE
FROM AWORKORDER
LEFT JOIN AWORKORDERITEM ON AWORKORDER.ID = AWORKORDERITEM.WORKORDERID
LEFT JOIN AWORKORDERSTATUS ON AWORKORDER.LASTSTATUSID = AWORKORDERSTATUS.ID
LEFT JOIN AWORKORDERITEMUNIT ON AWORKORDERITEM.ID = AWORKORDERITEMUNIT.WORKORDERITEMID
WHERE AWORKORDERITEMUNIT.ID = {o.Id}
AND AWORKORDERSTATUS.COMPLETED = TRUE
ORDER BY AWORKORDER.ID DESC
LIMIT 1";
using (var dr = await command.ExecuteReaderAsync())
if (await dr.ReadAsync())
{
o.LastCompletedWorkOrderViz = dr.GetInt64(0);
o.LastCompletedServiceDateViz = dr.GetDateTime(1);
}
await ct.Database.CloseConnectionAsync();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest)
{
//for now just re-use the report data code
//this may turn out to be the pattern for most biz object types but keeping it seperate allows for custom usage from time to time
return await GetReportData(dataListSelectedRequest);
}
public async Task<List<string>> ImportData(JArray ja)
{
List<string> ImportResult = new List<string>();
string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";
var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
foreach (JObject j in ja)
{
var w = j.ToObject<Unit>(jsset);
if (j["CustomFields"] != null)
w.CustomFields = j["CustomFields"].ToString();
w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
var res = await CreateAsync(w);
if (res == null)
{
ImportResult.Add($"* {w.Serial} - {this.GetErrorsAsString()}");
this.ClearErrors();
}
else
{
ImportResult.Add($"{w.Serial} - ok");
}
}
return ImportResult;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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($"UnitBiz.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.Unit.AsNoTracking().Select(z => z.Id).ToListAsync();
bool SaveIt = false;
foreach (long id in idList)
{
try
{
SaveIt = false;
ClearErrors();
Unit 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++;
}
}
}
catch (Exception ex)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
}
}
await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task HandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<UnitBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
Unit o = (Unit)proposedObj;
o.Name = o.Serial;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
//## 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, AyaType.Unit, o.Id, NotifyEventType.UnitWarrantyExpiry);
await NotifyEventHelper.ClearPriorEventsForObject(ct, AyaType.Unit, o.Id, NotifyEventType.ContractExpiring);
//## CREATED / MODIFIED EVENTS
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
{
//# UNIT WARRANTY EXPIRY
{
//does the unit even have a model or warranty?
int effectiveWarrantyMonths = 0;
//unit has own warranty terms
if (o.OverrideModelWarranty && !o.LifeTimeWarranty && o.WarrantyLength != null && o.WarrantyLength > 0)
{
effectiveWarrantyMonths = (int)o.WarrantyLength;
}
else
{
//unit has model based warranty terms
if (o.UnitModelId != null)
{
UnitModel um = await ct.UnitModel.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.UnitModelId);
if (!um.LifeTimeWarranty && um.WarrantyLength != null && um.WarrantyLength > 0)
{
effectiveWarrantyMonths = (int)um.WarrantyLength;
}
}
}
if (effectiveWarrantyMonths > 0)
{
var WarrantyExpirydate = DateTime.UtcNow.AddMonths(effectiveWarrantyMonths);
//notify users about warranty expiry (time delayed)
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.UnitWarrantyExpiry).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(o.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.UnitWarrantyExpiry,
UserId = sub.UserId,
AyaType = o.AyaType,
ObjectId = o.Id,
NotifySubscriptionId = sub.Id,
Name = o.Serial,
EventDate = WarrantyExpirydate
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}
}//warranty expiry event
//# CONTRACT EXPIRY
{
if (o.ContractId != null && o.ContractExpires != null)
{
var ContractExpirydate = (DateTime)o.ContractExpires;
//notify users about contract expiry (time delayed)
var subs = await ct.NotifySubscription.Where(z => z.EventType == NotifyEventType.ContractExpiring).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(o.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ContractExpiring,
UserId = sub.UserId,
AyaType = o.AyaType,
ObjectId = o.Id,
NotifySubscriptionId = sub.Id,
Name = o.Serial,
EventDate = ContractExpirydate
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}
}//Contract expiry event
}
}//end of process notifications
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons