Files
raven/server/AyaNova/biz/UnitMeterReadingBiz.cs
2021-08-23 23:25:20 +00:00

281 lines
13 KiB
C#

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;
using System.Collections.Generic;
using Newtonsoft.Json;
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<bool> ExistsAsync(long id)
{
return await ct.UnitMeterReading.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<UnitMeterReading> 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<UnitMeterReading> 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<Search.SearchIndexProcessObjectParameters> GetSearchResultSummary(long id)
{
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<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.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;
foreach (UnitMeterReading w in orderedList)
{
await PopulateVizFields(w);
var jo = JObject.FromObject(w);
ReportData.Add(jo);
}
}
return ReportData;
}
//populate viz fields from provided object
private async Task PopulateVizFields(UnitMeterReading o)
{
o.UnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync();
if (o.WorkOrderItemUnitId != null)
o.WorkOrderViz = (await WorkOrderBiz.GetWorkOrderSerialFromRelativeAsync(AyaType.WorkOrderItemUnit, (long)o.WorkOrderItemUnitId, ct)).ToString();
}
////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task HandlePotentialNotificationEvent(AyaEvent ayaEvent, UnitMeterReading proposedObj)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<UnitMeterReadingBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]");
await Task.CompletedTask;
//SPECIFIC EVENTS FOR THIS OBJECT
//#todo: METER READING EVENT
//MIGRATE_OUTSTANDING need meter reading object to complete unit notification for UnitMeterReadingMultipleExceeded
//UnitMeterReadingMultipleExceeded = 26,//* UnitMeterReading object, Created, conditional on DecValue as the Multiple threshold, if passed then notifies
//{
//first remove any existing, potentially stale notifyevents for this exact object and notifyeventtype
//await NotifyEventHelper.ClearPriorEventsForObject(ct, AyaType.Unit, o.Id, NotifyEventType.UnitMeterReadingMultipleExceeded);
//then check if unit is still metered etc etc and do the rest once the unit meter reading is coded
//}
/*
This could work as a threshold multiple value, i.e. if it's passing a multiple of 50,000 then notify, or notify every 10,000 or something.
In saving code it just checks to see if it has crossed the multiple threshold, i.e. calculate the old multiple, calculate the new multiple if over then send notification immediate.
Multiple is 10:
old was 5, new is 7 so 7-5 < 10 so do nothing
old was 5, new is 12 so 12-5=7 < 10 so do nothing
old was 5, new is 16 so 16-5==11 > 10 so do notification
*/
{
//see if any subscribers
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.UnitMeterReadingMultipleExceeded).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) 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)
.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
var diff = Math.Abs(proposedObj.Meter - lastReading);
if (sub.DecValue < diff)
{
//notification is a go
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.WorkorderTotalExceedsThreshold,
UserId = sub.UserId,
AyaType = AyaType.WorkOrder,
ObjectId = oProposed.WorkOrderId,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()}",
DecValue = GrandTotal
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}
}//end of process notifications
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons