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; namespace AyaNova.Biz { internal class UnitBiz : BizObject, ISearchAbleObject, 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.BizAdminFull); } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExistsAsync(long id) { return await ct.Unit.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE // internal async Task 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; } } //////////////////////////////////////////////////////////////////////////////////////////////// //DUPLICATE // internal async Task DuplicateAsync(long id) { Unit dbObject = await GetAsync(id, false); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } Unit newObject = new Unit(); CopyObject.Copy(dbObject, newObject, "Wiki"); string newUniqueName = string.Empty; bool NotUnique = true; long l = 1; do { newUniqueName = Util.StringUtil.UniqueNameBuilder(dbObject.Serial, l++, 255); NotUnique = await ct.Unit.AnyAsync(z => z.Serial == newUniqueName); } while (NotUnique); newObject.Serial = newUniqueName; newObject.Id = 0; newObject.Concurrency = 0; 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 GetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.Unit.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 PutAsync(Unit putObject) { Unit dbObject = await ct.Unit.SingleOrDefaultAsync(z => z.Id == putObject.Id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND, "id"); return null; } Unit SnapshotOfOriginalDBObj = new Unit(); CopyObject.Copy(dbObject, SnapshotOfOriginalDBObj); CopyObject.Copy(putObject, dbObject, "Id"); dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags); dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields); ct.Entry(dbObject).OriginalValues["Concurrency"] = putObject.Concurrency; await ValidateAsync(dbObject, SnapshotOfOriginalDBObj); if (HasErrors) return null; 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(dbObject, false); await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags); await HandlePotentialNotificationEvent(AyaEvent.Modified, dbObject, SnapshotOfOriginalDBObj); return dbObject; } //////////////////////////////////////////////////////////////////////////////////////////////// //DELETE // internal async Task DeleteAsync(long id) { using (var transaction = await ct.Database.BeginTransactionAsync()) { try { Unit dbObject = await ct.Unit.SingleOrDefaultAsync(z => z.Id == id); if (dbObject == null) { AddError(ApiErrorCode.NOT_FOUND); return false; } ValidateCanDelete(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); } catch { //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } 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 GetSearchResultSummary(long id) { var obj = await ct.Unit.SingleOrDefaultAsync(z => z.Id == id); 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"); } } //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 void ValidateCanDelete(Unit inObj) { //whatever needs to be check to delete this object } //////////////////////////////////////////////////////////////////////////////////////////////// // NOTIFICATION PROCESSING // public async Task HandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null) { ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger(); if (ServerBootConfig.SEEDING) 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 /* UnitWarrantyExpiry = 25,//* Unit object created, advance notice can be used, tag conditional */ //## DELETED EVENTS //TODO: there are no specific deleted events //but 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 //## CREATED / MODIFIED EVENTS if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified) { Unit o = (Unit)proposedObj; //# UNIT WARRANTY EXPIRY { //first remove any existing, potentially stale notifyevents for this exact object and notifyeventtype await NotifyEventHelper.ClearPriorEventsForObject(ct, AyaType.Unit, o.Id, NotifyEventType.UnitWarrantyExpiry); //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.TagsMatch(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 //#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 //} } }//end of process notifications ///////////////////////////////////////////////////////////////////// }//eoc }//eons