using System; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using System.Linq; using AyaNova.Util; using AyaNova.Api.ControllerHelpers; using Microsoft.Extensions.Logging; using AyaNova.Models; using Newtonsoft.Json.Linq; namespace AyaNova.Biz { internal class UnitMeterReadingBiz : BizObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject { internal UnitMeterReadingBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = UserRoles; BizType = AyaType.UnitMeterReading; } internal static UnitMeterReadingBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) { if (httpContext != null) return new UnitMeterReadingBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); else return new UnitMeterReadingBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdmin); } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExistsAsync(long id) { return await ct.UnitMeterReading.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task CreateAsync(UnitMeterReading newObject) { await ValidateAsync(newObject); if (HasErrors) return null; else { await ct.UnitMeterReading.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await SearchIndexAsync(newObject, true); await HandlePotentialNotificationEvent(AyaEvent.Created, newObject); return newObject; } } //////////////////////////////////////////////////////////////////////////////////////////////// //GET // internal async Task GetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.UnitMeterReading.AsNoTracking().SingleOrDefaultAsync(m => m.Id == id); if (logTheGetEvent && ret != null) await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct); return ret; } //////////////////////////////////////////////////////////////////////////////////////////////// //SEARCH // private async Task SearchIndexAsync(UnitMeterReading 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, AyaType specificType) { var obj = await GetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); DigestSearchText(obj, SearchParams); return SearchParams; } public void DigestSearchText(UnitMeterReading obj, Search.SearchIndexProcessObjectParameters searchParams) { if (obj != null) searchParams.AddText(obj.Notes) .AddText(obj.Meter); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ValidateAsync(UnitMeterReading proposedObj) { //Unit required var unit = await ct.Unit.AsNoTracking().FirstOrDefaultAsync(z => z.Id == proposedObj.UnitId); if (unit == null) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId", "Unit not found with that id");//api issue not user issue so no need to translate return; } if (!unit.Metered) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId", "This is not a metered unit, reading cannot be saved");//api issue not user issue so no need to translate return; } if (proposedObj.WorkOrderItemUnitId != null) { if (!await ct.WorkOrderItemUnit.AnyAsync(z => z.Id == proposedObj.WorkOrderItemUnitId)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemUnitId", "work order item unit record was not found");//api issue not user issue so no need to translate return; } } //No negative amounts allowed, a meter can count down to zero or up to whatever but it can't go negative or the math breaks down if (proposedObj.Meter < 0) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Meter", "Negative meter readings not supported"); } } //////////////////////////////////////////////////////////////////////////////////////////////// //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.UnitMeterReading.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 (UnitMeterReading w in orderedList) { if (!ReportRenderManager.KeepGoing(jobId)) return null; await PopulateVizFields(w); var jo = JObject.FromObject(w); ReportData.Add(jo); } orderedList = null; } vc.Clear(); return ReportData; } private VizCache vc = new VizCache(); //populate viz fields from provided object private async Task PopulateVizFields(UnitMeterReading o) { if (!vc.Has("unitserial", o.UnitId)) vc.Add(await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync(), "unitserial", o.UnitId); o.UnitViz = vc.Get("unitserial", o.UnitId); if (o.WorkOrderItemUnitId != null) { if (!vc.Has("woserial", o.WorkOrderItemUnitId)) vc.Add((await WorkOrderBiz.GetWorkOrderSerialFromRelativeAsync(AyaType.WorkOrderItemUnit, (long)o.WorkOrderItemUnitId, ct)).ToString(), "woserial", o.WorkOrderItemUnitId); o.WorkOrderViz = vc.Get("woserial", o.WorkOrderItemUnitId); } } //////////////////////////////////////////////////////////////////////////////////////////////// // IMPORT EXPORT // public async Task 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); } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task HandlePotentialNotificationEvent(AyaEvent ayaEvent, UnitMeterReading proposedObj) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return; log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]"); //SPECIFIC EVENTS FOR THIS OBJECT //UnitMeterReadingMultipleExceeded event { //see if any subscribers var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.UnitMeterReadingMultipleExceeded).ToListAsync(); if (subs.Count == 0) return; var unitInfo = await ct.Unit.AsNoTracking().Where(x => x.Id == proposedObj.UnitId).Select(x => new { x.Name, x.Serial, x.Tags }).FirstOrDefaultAsync(); foreach (var sub in subs) { //not for inactive users if (!await UserBiz.UserIsActive(sub.UserId)) continue; if (!NotifyEventHelper.ObjectHasAllSubscriptionTags(unitInfo.Tags, sub.Tags)) continue; //is multiple exceeded?? //Formula is to retrieve last highest reading, if it's not found then it's zero //subtract last meter reading from current meter reading and if it's equal or more than threshold trigger notification var lastMeterReading = await ct.UnitMeterReading.AsNoTracking() .Where(x => x.UnitId == proposedObj.UnitId) .OrderByDescending(z => z.MeterDate)//note: if in future there is an issue with not being most recent this sb id instead of date but if date is manually updatable then no .Skip(1)//because it's already saved this current reading by the time it gets here .Take(1) .FirstOrDefaultAsync(); long lastReading = 0; if (lastMeterReading != null) lastReading = lastMeterReading.Meter; //NOTE: this will cover scenarios where the meter wraps back to zero or is reset to zero or a meter counts **down** instead of up //meter readings are always positive due to biz rule, dec value is always within long range and greater than 4 (arbitrarily) long diff = Math.Abs(proposedObj.Meter - lastReading); if (diff >= sub.DecValue) { //notification is a go NotifyEvent n = new NotifyEvent() { EventType = NotifyEventType.UnitMeterReadingMultipleExceeded, UserId = sub.UserId, AyaType = AyaType.UnitMeterReading, ObjectId = proposedObj.Id, NotifySubscriptionId = sub.Id, Name = $"{unitInfo.Serial}, {unitInfo.Name}", DecValue = diff }; await ct.NotifyEvent.AddAsync(n); log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]"); await ct.SaveChangesAsync(); } } } }//end of process notifications ///////////////////////////////////////////////////////////////////// }//eoc }//eons