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; using Microsoft.EntityFrameworkCore.Storage; namespace AyaNova.Biz { internal class PartInventoryBiz : BizObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject { internal PartInventoryBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles) { ct = dbcontext; UserId = currentUserId; UserTranslationId = userTranslationId; CurrentUserRoles = UserRoles; BizType = AyaType.PartInventory; } internal static PartInventoryBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null) { if (httpContext != null) return new PartInventoryBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items)); else return new PartInventoryBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull); } //////////////////////////////////////////////////////////////////////////////////////////////// //EXISTS internal async Task ExistsAsync(long id) { return await ct.PartInventory.AnyAsync(z => z.Id == id); } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE (public facing adjustment version) // internal async Task CreateAsync(dtPartInventory newDtObject) { using (var transaction = await ct.Database.BeginTransactionAsync()) { try { //get the last record if exists (will not if opening balance) var LastEntry = await ct.PartInventory.OrderByDescending(m => m.EntryDate).FirstOrDefaultAsync(m => m.PartId == newDtObject.PartId && m.PartWarehouseId == newDtObject.PartWarehouseId); PartInventory newObject = new PartInventory(); newObject.Description = newDtObject.Description; newObject.EntryDate = DateTime.UtcNow; newObject.PartId = newDtObject.PartId; newObject.PartWarehouseId = newDtObject.PartWarehouseId; newObject.SourceId = null; newObject.SourceType = null; newObject.Quantity = newDtObject.Quantity; if (LastEntry != null) { newObject.LastEntryDate = LastEntry.EntryDate; newObject.LastBalance = LastEntry.Balance; newObject.Balance = LastEntry.Balance + newObject.Quantity; } else { newObject.Balance = newObject.Quantity; } await ValidateAsync(newObject); if (HasErrors) return null; else { await ct.PartInventory.AddAsync(newObject); await ct.SaveChangesAsync(); await transaction.CommitAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await SearchIndexAsync(newObject, true); return newObject; } } catch { //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } } } //////////////////////////////////////////////////////////////////////////////////////////////// //CREATE (internal version) // internal async Task CreateAsync(dtPOPartInventory newDtObject) { try { //get the last record if exists (will not if opening balance) var LastEntry = await ct.PartInventory.OrderByDescending(m => m.EntryDate).FirstOrDefaultAsync(m => m.PartId == newDtObject.PartId && m.PartWarehouseId == newDtObject.PartWarehouseId); PartInventory newObject = new PartInventory(); newObject.Description = newDtObject.Description; newObject.EntryDate = DateTime.UtcNow; newObject.PartId = newDtObject.PartId; newObject.PartWarehouseId = newDtObject.PartWarehouseId; newObject.SourceId = newDtObject.SourceId; newObject.SourceType = newDtObject.SourceType; newObject.Quantity = newDtObject.Quantity; if (LastEntry != null) { newObject.LastEntryDate = LastEntry.EntryDate; newObject.LastBalance = LastEntry.Balance; newObject.Balance = LastEntry.Balance + newObject.Quantity; } else { newObject.Balance = newObject.Quantity; } await ValidateAsync(newObject); if (HasErrors) return null; else { await ct.PartInventory.AddAsync(newObject); await ct.SaveChangesAsync(); await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct); await SearchIndexAsync(newObject, true); return newObject; } } catch { //Just re-throw for now, let exception handler deal, but in future may want to deal with this more here throw; } } //////////////////////////////////////////////////////////////////////////////////////////////// //GET // internal async Task GetAsync(long id, bool logTheGetEvent = true) { var ret = await ct.PartInventory.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(PartInventory 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 GetAsync(id, false); var SearchParams = new Search.SearchIndexProcessObjectParameters(); DigestSearchText(obj, SearchParams); return SearchParams; } public void DigestSearchText(PartInventory obj, Search.SearchIndexProcessObjectParameters searchParams) { if (obj != null) searchParams.AddText(obj.Description); } //////////////////////////////////////////////////////////////////////////////////////////////// //VALIDATION // private async Task ValidateAsync(PartInventory proposedObj) { //NOTE: Many of these errors wouldn't happen in the official ayanova UI (api developer error only) so are not translated //Description required (only affordance to open the record) if (string.IsNullOrWhiteSpace(proposedObj.Description)) AddError(ApiErrorCode.VALIDATION_REQUIRED, "Description"); if (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.Part, proposedObj.PartId, ct)) { AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"PartInventory Part specified doesn't exist [id:{proposedObj.PartId}]"); return; } if (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.PartWarehouse, proposedObj.PartWarehouseId, ct)) { AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"PartInventory PartWarehouse specified doesn't exist [id:{proposedObj.PartWarehouseId}]"); return; } //Source id and type must either both be null or neither be null if ((proposedObj.SourceId == null && proposedObj.SourceType != null) || (proposedObj.SourceId != null && proposedObj.SourceType == null)) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "generalerror", "Source type and id must be specified or both null for a manual adjustment"); return; } if (proposedObj.SourceType != null && proposedObj.SourceId != null && !await BizObjectExistsInDatabase.ExistsAsync((AyaType)proposedObj.SourceType, (long)proposedObj.SourceId, ct)) { AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"PartInventory source object causing inventory change specified doesn't exist [type:{proposedObj.SourceType}, id:{proposedObj.SourceId}]"); return; } //New entry must have *something* to bank if (proposedObj.Quantity == 0) { AddError(ApiErrorCode.VALIDATION_REQUIRED, "Quantity"); return; } //values must add up if (proposedObj.Balance != (proposedObj.Quantity + (proposedObj.LastBalance ?? 0))) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Balance", "Balance incorrect (LastBalance + Quantity not equal to Balance"); return; } //date is newer than last entry date? if (proposedObj.LastEntryDate != null && proposedObj.LastEntryDate > proposedObj.EntryDate) { AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "generalerror", "LastEntryDate is newer than EntryDate"); return; } //valid previous columns? //either they're all null or none of them are null if (!((proposedObj.LastEntryDate == null && proposedObj.LastBalance == null) || (proposedObj.LastEntryDate != null && proposedObj.LastBalance != null) )) { AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "generalerror", "Last* entries must be all empty (opening entry) or none of them empty (any later entry)"); return; } } //////////////////////////////////////////////////////////////////////////////////////////////// //REPORTING // public async Task GetReportData(long[] idList) { 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.PartInventory.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; //cache frequent viz data var AyaTypesEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList( StringUtil.TrimTypeName(typeof(AyaType).ToString()), UserTranslationId, CurrentUserRoles); using (var command = ct.Database.GetDbConnection().CreateCommand()) { ct.Database.OpenConnection(); foreach (PartInventory w in orderedList) { await PopulateVizFields(w, AyaTypesEnumList, command); 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(PartInventory o, List ayaTypesEnumList, System.Data.Common.DbCommand cmd) { o.PartViz = await ct.Part.AsNoTracking().Where(x => x.Id == o.PartId).Select(x => x.PartNumber).FirstOrDefaultAsync(); o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(); if (o.SourceType != null) o.SourceTypeViz = ayaTypesEnumList.Where(x => x.Id == (long)o.SourceType).Select(x => x.Name).First(); if (o.SourceType != null && o.SourceId != null) o.SourceViz = BizObjectNameFetcherDirect.Name((AyaType)o.SourceType, (long)o.SourceId, cmd); } //////////////////////////////////////////////////////////////////////////////////////////////// // IMPORT EXPORT // public async Task GetExportData(long[] idList) { //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(idList); } ///////////////////////////////////////////////////////////////////// }//eoc }//eons