Files
raven/server/AyaNova/biz/PartInventoryBiz.cs
John Cardinal 59fd41d8d0 CHECKPOINT COMMIT - POST SERVICE BANK REMOVAL
Service bank feature removed from front, back and e2e testing
mostly commented out in case need to add back again but in some places such as db schema it had to be removed entirely
so refer here if adding back in again
2021-06-04 22:23:21 +00:00

343 lines
15 KiB
C#

using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
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<bool> ExistsAsync(long id)
{
return await ct.PartInventory.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE (public facing adjustment version)
//
internal async Task<PartInventory> 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<PartInventory> CreateAsync(dtInternalPartInventory 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<PartInventory> 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<Search.SearchIndexProcessObjectParameters> 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* that would affect quantities
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;
}
//Balance must not be negative
//return the current balance
if (proposedObj.Balance < 0)
{
string currentBalance = "0";
if (proposedObj.LastBalance != null)
currentBalance = ((decimal)proposedObj.LastBalance).ToString("G29");
AddError(ApiErrorCode.INSUFFICIENT_INVENTORY, "Balance", $"{currentBalance}");
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<JArray> 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<NameIdItem> 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<JArray> 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