Files
raven/server/AyaNova/biz/PartInventoryBiz.cs
2021-01-22 00:11:08 +00:00

281 lines
12 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 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 (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 = 0;
newObject.SourceType = AyaType.NoType;
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);
await transaction.CommitAsync();
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(PartInventory newObject)
{
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;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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)
{
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;
}
if (proposedObj.SourceType != AyaType.NoType && proposedObj.SourceId != 0 && !await BizObjectExistsInDatabase.ExistsAsync(proposedObj.SourceType, 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;
}
/*
"CONSTRAINT UNQ_PartInventory UNIQUE (sourceid, sourcetype, entrydate, balance), " +
"CONSTRAINT UNQ_PartInventory_Previous_values UNIQUE (sourceid, sourcetype, lastentrydate, lastbalance), " +
"CONSTRAINT fk_PartInventory_self FOREIGN KEY (sourceid, sourcetype, lastentrydate, lastbalance) references apartinventory(sourceid, sourcetype, entrydate, balance), " +
"CONSTRAINT CHK_PartInventory_Valid_Balance CHECK(balance >= 0 AND (balance = COALESCE(lastbalance, 0) + quantity)), " +
"CONSTRAINT CHK_PartInventory_Valid_Dates_Sequence CHECK(lastentrydate < entrydate), " +
"CONSTRAINT CHK_PartInventory_Valid_Previous_Columns CHECK((lastentrydate IS NULL AND lastbalance IS NULL) OR (lastentrydate IS NOT NULL AND lastbalance IS NOT NULL)) " +
*/
//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
//fix and re-test
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;
foreach (PartInventory w in orderedList)
{
var jo = JObject.FromObject(w);
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
ReportData.Add(jo);
}
}
return ReportData;
}
////////////////////////////////////////////////////////////////////////////////////////////////
// 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