Files
raven/server/AyaNova/biz/WorkOrderBiz.cs
2021-06-14 20:05:02 +00:00

5934 lines
286 KiB
C#

using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.EntityFrameworkCore.Storage;
using AyaNova.Util;
using AyaNova.Api.ControllerHelpers;
using AyaNova.Models;
using System.Linq;
using System;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
namespace AyaNova.Biz
{
internal class WorkOrderBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject
{
// //Feature specific roles
// internal static AuthorizationRoles RolesAllowedToChangeSerial = AuthorizationRoles.BizAdminFull | AuthorizationRoles.DispatchFull | AuthorizationRoles.AccountingFull;
internal WorkOrderBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = AyaType.WorkOrder;
}
internal static WorkOrderBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new WorkOrderBiz(ct, UserIdFromContext.Id(httpContext.Items), UserTranslationIdFromContext.Id(httpContext.Items), UserRolesFromContext.Roles(httpContext.Items));
else
return new WorkOrderBiz(ct, 1, ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID, AuthorizationRoles.BizAdminFull);
}
/*
██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██████╗ ███████╗██████╗
██║ ██║██╔═══██╗██╔══██╗██║ ██╔╝ ██╔═══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗
██║ █╗ ██║██║ ██║██████╔╝█████╔╝█████╗██║ ██║██████╔╝██║ ██║█████╗ ██████╔╝
██║███╗██║██║ ██║██╔══██╗██╔═██╗╚════╝██║ ██║██╔══██╗██║ ██║██╔══╝ ██╔══██╗
╚███╔███╔╝╚██████╔╝██║ ██║██║ ██╗ ╚██████╔╝██║ ██║██████╔╝███████╗██║ ██║
╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ ╚══════╝╚═╝ ╚═╝
*/
#region WorkOrder level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> WorkOrderExistsAsync(long id)
{
return await ct.WorkOrder.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrder> WorkOrderCreateAsync(WorkOrder newObject, bool populateViz = true)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
await WorkOrderValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await WorkOrderBizActionsAsync(AyaEvent.Created, newObject, null, null);
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrder.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
await WorkOrderSearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
//# NOTE: only internal code can post an entire workorder graph, no external user can as controller will reject right up front
//however, internally seeder will post entire workorders
if (newObject.Items.Count > 0)
{
await GetCurrentContractFromContractIdAsync(newObject.ContractId);
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
newObject.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
//GRANDCHILD BIZ ACTIONS
foreach (WorkOrderItem wi in newObject.Items)
{
foreach (WorkOrderItemPart wip in wi.Parts)
await PartBizActionsAsync(AyaEvent.Created, wip, null, null);
foreach (WorkOrderItemLoan wil in wi.Loans)
await LoanBizActionsAsync(AyaEvent.Created, wil, null, null);
}
await ct.SaveChangesAsync();
//INVENTORY ADJUSTMENTS
foreach (WorkOrderItem wi in newObject.Items)
{
foreach (WorkOrderItemPart wip in wi.Parts)
{
await PartInventoryAdjustmentAsync(AyaEvent.Created, wip, null, transaction);
if (HasErrors)
{
await transaction.RollbackAsync();
return null;
}
}
}
//NOTE: not running individual notification here for children, seeder won't require it and that's all that posts an entire wo currently
}
await transaction.CommitAsync();
if (populateViz)
await WorkOrderPopulateVizFields(newObject, true);
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DUPLICATE
//
internal async Task<WorkOrder> WorkOrderDuplicateAsync(long id)
{
WorkOrder dbObject = await WorkOrderGetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
WorkOrder newObject = new WorkOrder();
CopyObject.Copy(dbObject, newObject, "Wiki, Serial, States");
//walk the tree and reset all id's and concurrencies
//TOP
newObject.Id = 0;
newObject.Concurrency = 0;
foreach (var o in newObject.Items)
{
o.Id = 0;
o.Concurrency = 0;
foreach (var v in o.Expenses)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.Labors)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.Loans)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.OutsideServices)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.PartRequests)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.Parts)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.ScheduledUsers)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.Tasks)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.Travels)
{ v.Id = 0; v.Concurrency = 0; }
foreach (var v in o.Units)
{ v.Id = 0; v.Concurrency = 0; }
}
await ct.WorkOrder.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
await WorkOrderSearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await WorkOrderPopulateVizFields(newObject, false);//doing this here ahead of notification because notification may require the viz field lookup anyway and afaict no harm in it
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrder> WorkOrderGetAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true)
{
//Note: there could be rules checking here in future, i.e. can only get own workorder or something
//if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now
//https://docs.microsoft.com/en-us/ef/core/querying/related-data
//docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections
var ret =
await ct.WorkOrder.AsSplitQuery().AsNoTracking()
.Include(s => s.States)
.Include(w => w.Items.OrderBy(item => item.Sequence))
.ThenInclude(wi => wi.Expenses)
.Include(w => w.Items)
.ThenInclude(wi => wi.Labors)
.Include(w => w.Items)
.ThenInclude(wi => wi.Loans)
.Include(w => w.Items)
.ThenInclude(wi => wi.Parts)
.Include(w => w.Items)
.ThenInclude(wi => wi.PartRequests)
.Include(w => w.Items)
.ThenInclude(wi => wi.ScheduledUsers)
.Include(w => w.Items)
.ThenInclude(wi => wi.Tasks.OrderBy(t => t.Sequence))
.Include(w => w.Items)
.ThenInclude(wi => wi.Travels)
.Include(w => w.Items)
.ThenInclude(wi => wi.Units)
.Include(w => w.Items)
.ThenInclude(wi => wi.OutsideServices)
.SingleOrDefaultAsync(z => z.Id == id);
//todo: set isLocked from state
var stat = await GetCurrentWorkOrderStatusFromRelatedAsync(BizType, ret.Id);
ret.IsLockedAtServer = stat.Locked;
if (populateDisplayFields)
await WorkOrderPopulateVizFields(ret, false);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrder> WorkOrderPutAsync(WorkOrder putObject)
{
//## PUT HEADER ONLY, NO ALLOWANCE FOR PUT OF ENTIRE WORKORDER
//Note: this is intentionally not using the getasync because
//doing so would invoke the children which would then get deleted on save since putobject has no children
WorkOrder dbObject = await ct.WorkOrder.AsNoTracking().FirstOrDefaultAsync(z => z.Id == putObject.Id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields);
await WorkOrderValidateAsync(putObject, dbObject);
if (HasErrors)
return null;
await WorkOrderBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
long? newContractId = null;
if (putObject.ContractId != dbObject.ContractId)//manual change of contract
{
newContractId = putObject.ContractId;
await GetCurrentContractFromContractIdAsync(newContractId);
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
putObject.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
}
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await WorkOrderExistsAsync(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 WorkOrderSearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await WorkOrderPopulateVizFields(putObject, true);//doing this here ahead of notification because notification may require the viz field lookup anyway and afaict no harm in it
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> WorkOrderDeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
try
{
WorkOrder dbObject = await ct.WorkOrder.AsNoTracking().Where(z => z.Id == id).FirstOrDefaultAsync();// WorkOrderGetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
WorkOrderValidateCanDelete(dbObject);
if (HasErrors)
return false;
//States collection
if (!await StateDeleteAsync(id, transaction))
return false;
//collect the child id's to delete
var ItemIds = await ct.WorkOrderItem.AsNoTracking().Where(z => z.WorkOrderId == id).Select(z => z.Id).ToListAsync();
//Delete children
foreach (long ItemId in ItemIds)
if (!await ItemDeleteAsync(ItemId, transaction))
return false;
ct.WorkOrder.Remove(dbObject);
await ct.SaveChangesAsync();
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, dbObject.Serial.ToString(), ct);
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
await transaction.CommitAsync();
await WorkOrderHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
}
catch
{
//NOTE: no need to rollback the transaction, it will auto-rollback if not committed and it is disposed when it goes out of scope either way
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task WorkOrderBizActionsAsync(AyaEvent ayaEvent, WorkOrder newObj, WorkOrder oldObj, IDbContextTransaction transaction)
{
//automatic actions on record change, called AFTER validation and BEFORE save
//so changes here will be saved by caller
//currently no processing required except for created or modified at this time
if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
return;
//CREATION ACTIONS
if (ayaEvent == AyaEvent.Created)
{
await AutoSetContractAsync(newObj);
if (newObj.CompleteByDate == null)//need to account for a user manually selecting a specific close by date in advance indicating to ignore any auto sets
await AutoSetCloseByDateAsync(newObj);
await AutoSetAddressAsync(newObj);
return;
}
//MODIFIED ACTIONS
if (ayaEvent == AyaEvent.Modified)
{
//if customer changed then contractId must be re-checked
if (newObj.CustomerId != oldObj.CustomerId)
{
await AutoSetContractAsync(newObj);
await AutoSetAddressAsync(newObj);
}
if (newObj.ContractId != oldObj.ContractId)
await AutoSetCloseByDateAsync(newObj);
}
}
private async Task AutoSetAddressAsync(WorkOrder newObj)
{
if (newObj.CustomerId == 0)
return;
var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).FirstOrDefaultAsync();
if (cust == null)
return;
newObj.PostAddress = cust.PostAddress;
newObj.PostCity = cust.PostCity;
newObj.PostRegion = cust.PostRegion;
newObj.PostCountry = cust.PostCountry;
newObj.PostCode = cust.PostCode;
newObj.Address = cust.Address;
newObj.City = cust.City;
newObj.Region = cust.Region;
newObj.Country = cust.Country;
newObj.Latitude = cust.Latitude;
newObj.Longitude = cust.Longitude;
if (cust.BillHeadOffice == true && cust.HeadOfficeId != null)
{
var head = await ct.HeadOffice.AsNoTracking().Where(z => z.Id == cust.HeadOfficeId).FirstOrDefaultAsync();
if (head == null)
return;
newObj.PostAddress = head.PostAddress;
newObj.PostCity = head.PostCity;
newObj.PostRegion = head.PostRegion;
newObj.PostCountry = head.PostCountry;
newObj.PostCode = head.PostCode;
}
}
private async Task AutoSetContractAsync(WorkOrder newObj)
{
//first reset contract fetched flag so a fresh copy is taken
//in case it was set already by other operations
mFetchedContractAlready = false;
//CONTRACT AUTO SET
//failsafe
newObj.ContractId = null;
if (newObj.CustomerId != 0)
{
//precedence: unit->customer->headoffice
var cust = await ct.Customer.AsNoTracking().Where(z => z.Id == newObj.CustomerId).Select(z => new { headofficeId = z.HeadOfficeId, contractId = z.ContractId, contractExpires = z.ContractExpires }).FirstOrDefaultAsync();
//first set it to the customer one if available in case the ho one has expired then set the ho if applicable
if (cust.contractId != null && cust.contractExpires > DateTime.UtcNow)
newObj.ContractId = cust.contractId;
else if (cust.headofficeId != null)
{
var head = await ct.HeadOffice.AsNoTracking().Where(z => z.Id == cust.headofficeId).Select(z => new { contractId = z.ContractId, contractExpires = z.ContractExpires }).FirstOrDefaultAsync();
if (head.contractId != null && head.contractExpires > DateTime.UtcNow)
newObj.ContractId = head.contractId;
}
}
}
private async Task AutoSetCloseByDateAsync(WorkOrder newObj)
{
//called when there is a definite possibility of change of close by i.e. new contract, new customer, new workorder
//RESPONSE TIME / COMPLETE BY AUTO SET
//precedence: manually pre-set -> contract -> global biz
if (newObj.ContractId != null)
{
await GetCurrentContractFromContractIdAsync(newObj.ContractId);
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
{
newObj.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
return; //our work here is done
}
}
//not set yet, maybe the global default is the way...
if (AyaNova.Util.ServerGlobalBizSettings.WorkOrderCompleteByAge != TimeSpan.Zero)
newObj.CompleteByDate = DateTime.UtcNow.Add(AyaNova.Util.ServerGlobalBizSettings.WorkOrderCompleteByAge);
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //CONTRACT UPDATE
// //
// internal async Task<WorkOrder> ChangeContract(long workOrderId, long? newContractId)
// {
// //this is called by UI via contract change route for contract change only and expects wo back to update client ui
// var w = await ct.WorkOrder.FirstOrDefaultAsync(z => z.Id == workOrderId);
// if (w == null)
// {
// AddError(ApiErrorCode.NOT_FOUND, "id");
// return null;
// }
// if (newContractId != null && !await ct.Contract.AnyAsync(z => z.Id == newContractId))
// {
// AddError(ApiErrorCode.NOT_FOUND, "generalerror", $"Contract with id {newContractId} not found");
// return null;
// }
// w.ContractId = newContractId;
// await ct.SaveChangesAsync();
// await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, workOrderId, BizType, AyaEvent.Modified), ct);
// await GetCurrentContractFromContractIdAsync(newContractId);
// var updatedWorkOrder = await ProcessChangeOfContractAsync(workOrderId);
// await WorkOrderPopulateVizFields(updatedWorkOrder, false);
// return updatedWorkOrder;//return entire workorder
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//GET WORKORDER ID FROM DESCENDANT TYPE AND ID
//
internal static async Task<long> GetWorkOrderIdFromRelativeAsync(AyaType ayaType, long id, AyContext ct)
{
long woitemid = 0;
switch (ayaType)
{
case AyaType.WorkOrder:
return id;
case AyaType.WorkOrderItem:
woitemid = id;
break;
case AyaType.WorkOrderItemExpense:
woitemid = await ct.WorkOrderItemExpense.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderItemLabor:
woitemid = await ct.WorkOrderItemLabor.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderItemLoan:
woitemid = await ct.WorkOrderItemLoan.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderItemPart:
woitemid = await ct.WorkOrderItemPart.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderItemPartRequest:
woitemid = await ct.WorkOrderItemPartRequest.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderItemScheduledUser:
woitemid = await ct.WorkOrderItemScheduledUser.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderItemTask:
woitemid = await ct.WorkOrderItemTask.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderItemTravel:
woitemid = await ct.WorkOrderItemTravel.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderItemOutsideService:
woitemid = await ct.WorkOrderItemOutsideService.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
case AyaType.WorkOrderStatus:
return await ct.WorkOrderState.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderId).SingleOrDefaultAsync();
case AyaType.WorkOrderItemUnit:
woitemid = await ct.WorkOrderItemUnit.AsNoTracking().Where(z => z.Id == id).Select(z => z.WorkOrderItemId).SingleOrDefaultAsync();
break;
default:
throw new System.NotSupportedException($"WorkOrderBiz::GetAncestor -> AyaType {ayaType.ToString()} is not supported");
}
return await ct.WorkOrderItem.AsNoTracking()
.Where(z => z.Id == woitemid)
.Select(z => z.WorkOrderId)
.SingleOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task WorkOrderSearchIndexAsync(WorkOrder 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 ct.WorkOrder.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);//# NOTE intentionally not calling workorder get async here, don't need the whole graph
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
}
public void DigestSearchText(WorkOrder obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes).AddText(obj.Serial).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// "The Andy" notification helper
//
// (for now this is only for the notification exceeds total so only need one grand total of
// line totals, if in future need more can return a Record object instead with split out
// taxes, net etc etc)
//
private async Task<decimal> WorkorderGrandTotalAsync(long workOrderId, AyContext ct)
{
var wo = await ct.WorkOrder.AsNoTracking().AsSplitQuery()
.Include(w => w.Items.OrderBy(item => item.Sequence))
.ThenInclude(wi => wi.Expenses)
.Include(w => w.Items)
.ThenInclude(wi => wi.Labors)
.Include(w => w.Items)
.ThenInclude(wi => wi.Loans)
.Include(w => w.Items)
.ThenInclude(wi => wi.Parts)
.Include(w => w.Items)
.ThenInclude(wi => wi.Travels)
.Include(w => w.Items)
.ThenInclude(wi => wi.OutsideServices)
.SingleOrDefaultAsync(z => z.Id == workOrderId);
if (wo == null) return 0m;
decimal GrandTotal = 0m;
//update pricing
foreach (WorkOrderItem wi in wo.Items)
{
foreach (WorkOrderItemExpense o in wi.Expenses)
await ExpensePopulateVizFields(o, true);
foreach (WorkOrderItemLabor o in wi.Labors)
await LaborPopulateVizFields(o, true);
foreach (WorkOrderItemLoan o in wi.Loans)
await LoanPopulateVizFields(o, null, true);
foreach (WorkOrderItemPart o in wi.Parts)
await PartPopulateVizFields(o, true);
foreach (WorkOrderItemTravel o in wi.Travels)
await TravelPopulateVizFields(o, true);
foreach (WorkOrderItemOutsideService o in wi.OutsideServices)
await OutsideServicePopulateVizFields(o, true);
}
foreach (WorkOrderItem wi in wo.Items)
{
foreach (WorkOrderItemExpense o in wi.Expenses)
GrandTotal += o.LineTotalViz;
foreach (WorkOrderItemLabor o in wi.Labors)
GrandTotal += o.LineTotalViz;
foreach (WorkOrderItemLoan o in wi.Loans)
GrandTotal += o.LineTotalViz;
foreach (WorkOrderItemPart o in wi.Parts)
GrandTotal += o.LineTotalViz;
foreach (WorkOrderItemTravel o in wi.Travels)
GrandTotal += o.LineTotalViz;
foreach (WorkOrderItemOutsideService o in wi.OutsideServices)
GrandTotal += o.LineTotalViz;
}
return GrandTotal;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private async Task WorkOrderValidateAsync(WorkOrder proposedObj, WorkOrder currentObj)
{
//This may become necessary for v8migrate, leaving out for now
//skip validation if seeding
//if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
/*
todo: workorder status list first, it's a table of created items, keep properties from v7 but add the following properties:
SelectRoles - who can select the status (still shows if they can't select but that's the current status, like active does)
This is best handled at the client. It prefetches all the status out of the normal picklist process, more like how other things are separately handled now without a picklist
client then knows if a status is available or not and can process to only present available ones
#### Server can use a biz rule to ensure that it can't be circumvented
UI defaults to any role
DeselectRoles - who can unset this status (important for process control)
UI defaults to any role
CompletedStatus bool - this is a final status indicating all work on the workorder is completed, affects notification etc
UI defaults to false but when set to true auto sets lockworkorder to true (but user can just unset lockworkorder)
LockWorkorder - this status is considered read only and the workorder is locked
Just a read only thing, can just change status to "unlock" it
to support states where no one should work on a wo for whatever reason but it's not necessarily completed
e.g. "Hold for inspection", "On hold" generally etc
*/
// //Name required
// if (string.IsNullOrWhiteSpace(proposedObj.Name))
// AddError(ApiErrorCode.VALIDATION_REQUIRED, "Name");
// //If name is otherwise OK, check that name is unique
// if (!PropertyHasErrors("Name"))
// {
// //Use Any command is efficient way to check existance, it doesn't return the record, just a true or false
// if (await ct.WorkOrder.AnyAsync(z => z.Name == proposedObj.Name && z.Id != proposedObj.Id))
// {
// AddError(ApiErrorCode.VALIDATION_NOT_UNIQUE, "Name");
// }
// }
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrder.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 WorkOrderValidateCanDelete(WorkOrder dbObject)
{
//FOREIGN KEY CHECKS
//these are examples copied from customer for when other objects are actually referencing them
// if (await ct.User.AnyAsync(m => m.CustomerId == inObj.Id))
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("User"));
// if (await ct.Unit.AnyAsync(m => m.CustomerId == inObj.Id))
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Unit"));
// if (await ct.CustomerServiceRequest.AnyAsync(m => m.CustomerId == inObj.Id))
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("CustomerServiceRequest"));
// if (await ct.PurchaseOrder.AnyAsync(m => m.DropShipToCustomerId == inObj.Id))
// AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("PurchaseOrder"));
}
//############### NOTIFICATION TODO
/*
todo: workorder notifications remove #30 and #32 as redundant
WorkorderStatusChange = 4,//* Workorder object, any *change* of status including from no status (new) to a specific conditional status ID value
WorkorderStatusAge = 24,//* Workorder object Created / Updated, conditional on exact status selected IdValue, Tags conditional, advance notice can be set
//THESE TWO ARE REDUNDANT:
this is actually workorderstatuschange because can just pick any status under workorderstatuschange to be notified about
WorkorderCompleted = 30, //*travel work order is set to any status that is flagged as a "Completed" type of status. Customer & User
//This one could be accomplished with WorkorderStatusAge, just pick a Completed status and set a time frame and wala!
WorkorderCompletedFollowUp = 32, //* travel workorder closed status follow up again after this many TIMESPAN
todo: CHANGE WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged workorder status type in selected time span from creation of workorder
Change this to a new type that is based on so many days *without* being set to a particular status
but first check if tied to contract response time stuff, how that's handled
that's closeby date in v7 but isn't that deprecated now without a "close"?
maybe I do need the Completed status bool thing above
*/
////////////////////////////////////////////////////////////////////////////////////////////////
//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();
List<WorkOrder> batchResults = new List<WorkOrder>();
foreach (long batchId in batch)
batchResults.Add(await WorkOrderGetAsync(batchId, true, false));
//order the results back into original
var orderedList = from id in batch join z in batchResults on id equals z.Id select z;
//TODO: ##HERE## SEE ContractBiz for example implementation; will need to pre-cache some translation keys for enum lists etc, for example:
// private async Task TaskPopulateVizFields(WorkOrderItemTask o, List<NameIdItem> taskCompletionTypeEnumList = null)
foreach (WorkOrder w in orderedList)
{
//populate entire workorder graph
//await WorkOrderPopulateVizFields(w);
//this is done by the initial fetch now
var jo = JObject.FromObject(w);
//WorkOrder header custom fields
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
//WorkOrderItem custom fields
foreach (JObject jItem in jo["Items"])
{
if (!JsonUtil.JTokenIsNullOrEmpty(jItem["CustomFields"]))
jItem["CustomFields"] = JObject.Parse((string)jItem["CustomFields"]);
//WorkOrderItemUnit custom fields
foreach (JObject jUnit in jItem["Units"])
{
if (!JsonUtil.JTokenIsNullOrEmpty(jUnit["CustomFields"]))
jUnit["CustomFields"] = JObject.Parse((string)jUnit["CustomFields"]);
}
}
ReportData.Add(jo);
}
}
return ReportData;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task WorkOrderPopulateVizFields(WorkOrder o, bool headerOnly)
{
if (!headerOnly)
{
foreach (var v in o.States)
await StatePopulateVizFields(v);
foreach (var v in o.Items)
await ItemPopulateVizFields(v);
}
//popup Alert notes
//Customer notes first then others below
var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => new { AlertViz = x.PopUpNotes, CustomerViz = x.Name }).FirstOrDefaultAsync();
if (!string.IsNullOrWhiteSpace(custInfo.AlertViz))
{
o.AlertViz = $"{await Translate("Customer")}\n{custInfo.AlertViz}\n\n";
}
o.CustomerViz = custInfo.CustomerViz;
if (o.ProjectId != null)
o.ProjectViz = await ct.Project.AsNoTracking().Where(x => x.Id == o.ProjectId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.ContractId != null)
{
var contractVizFields = await ct.Contract.AsNoTracking().Where(x => x.Id == o.ContractId).Select(x => new { Name = x.Name, AlertNotes = x.AlertNotes }).FirstOrDefaultAsync();
o.ContractViz = contractVizFields.Name;
if (!string.IsNullOrWhiteSpace(contractVizFields.AlertNotes))
{
o.AlertViz += $"{await Translate("Contract")}\n{contractVizFields.AlertNotes}\n\n";
}
}
else
o.ContractViz = "-";
}
////////////////////////////////////////////////////////////////////////////////////////////////
// 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);
}
// public async Task<List<string>> ImportData(JArray ja)
// {
// List<string> ImportResult = new List<string>();
// string ImportTag = $"imported-{FileUtil.GetSafeDateFileName()}";
// var jsset = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new AyaNova.Util.JsonUtil.ShouldSerializeContractResolver(new string[] { "Concurrency", "Id", "CustomFields" }) });
// foreach (JObject j in ja)
// {
// var w = j.ToObject<WorkOrder>(jsset);
// if (j["CustomFields"] != null)
// w.CustomFields = j["CustomFields"].ToString();
// w.Tags.Add(ImportTag);//so user can find them all and revert later if necessary
// var res = await WorkOrderCreateAsync(w);
// if (res == null)
// {
// ImportResult.Add($"* {w.Serial} - {this.GetErrorsAsString()}");
// this.ClearErrors();
// }
// else
// {
// ImportResult.Add($"{w.Serial} - ok");
// }
// }
// return ImportResult;
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
public async Task HandleJobAsync(OpsJob job)
{
switch (job.JobType)
{
case JobType.BatchCoreObjectOperation:
await ProcessBatchJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"WorkOrder.HandleJob-> Invalid job type{job.JobType.ToString()}");
}
}
private async Task ProcessBatchJobAsync(OpsJob job)
{
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Running);
await JobsBiz.LogJobAsync(job.GId, $"LT:StartJob {job.SubType}");
List<long> idList = new List<long>();
long FailedObjectCount = 0;
JObject jobData = JObject.Parse(job.JobInfo);
if (jobData.ContainsKey("idList"))
idList = ((JArray)jobData["idList"]).ToObject<List<long>>();
else
idList = await ct.Widget.Select(z => z.Id).ToListAsync();
bool SaveIt = false;
foreach (long id in idList)
{
try
{
SaveIt = false;
ClearErrors();
ICoreBizObjectModel o = null;
//save a fetch if it's a delete
if (job.SubType != JobSubType.Delete)
o = await GetWorkOrderGraphItem(job.AType, id);
switch (job.SubType)
{
case JobSubType.TagAddAny:
case JobSubType.TagAdd:
case JobSubType.TagRemoveAny:
case JobSubType.TagRemove:
case JobSubType.TagReplaceAny:
case JobSubType.TagReplace:
SaveIt = TagBiz.ProcessBatchTagOperation(o.Tags, (string)jobData["tag"], jobData.ContainsKey("toTag") ? (string)jobData["toTag"] : null, job.SubType);
break;
case JobSubType.Delete:
if (!await DeleteWorkOrderGraphItem(job.AType, id))
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
break;
default:
throw new System.ArgumentOutOfRangeException($"ProcessBatchJobAsync -> Invalid job Subtype{job.SubType}");
}
if (SaveIt)
{
o = await PutWorkOrderGraphItem(job.AType, o);
if (o == null)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors {GetErrorsAsString()} id {id}");
FailedObjectCount++;
}
}
}
catch (Exception ex)
{
await JobsBiz.LogJobAsync(job.GId, $"LT:Errors id({id})");
await JobsBiz.LogJobAsync(job.GId, ExceptionUtil.ExtractAllExceptionMessages(ex));
}
}
await JobsBiz.LogJobAsync(job.GId, $"LT:BatchJob {job.SubType} {idList.Count}{(FailedObjectCount > 0 ? " - LT:Failed " + FailedObjectCount : "")}");
await JobsBiz.UpdateJobStatusAsync(job.GId, JobStatus.Completed);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task WorkOrderHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrder oProposed = (WorkOrder)proposedObj;
proposedObj.Name = oProposed.Serial.ToString();
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);//Note: will properly handle all delete events and event removal if deleted
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrder oCurrent = null;
bool SameTags = true;
if (currentObj != null)
{
oCurrent = (WorkOrder)currentObj;
SameTags = NotifyEventHelper.TwoObjectsHaveSameTags(proposedObj.Tags, currentObj.Tags);
}
#region COMPLETE BY OVERDUE
if (ayaEvent == AyaEvent.Created && oProposed.CompleteByDate != null)
{
//WorkorderCompletedStatusOverdue Created here on workorder creation for any subscribers
//State notify event processor below has more notes and details and will remove this event if set to a completed state
//If new and has completeby then can do notification
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompletedStatusOverdue).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.ObjectHasAllSubscriptionTags(proposedObj.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.WorkorderCompletedStatusOverdue,
UserId = sub.UserId,
AyaType = proposedObj.AyaType,
ObjectId = proposedObj.Id,
NotifySubscriptionId = sub.Id,
Name = oProposed.Serial.ToString(),
EventDate = (DateTime)oProposed.CompleteByDate
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//CREATED overdue completion
if (ayaEvent == AyaEvent.Modified)
{// WorkorderCompletedStatusOverdue modified in some way, could be tags, could be date either of which is relevant to this notification block
//differences requiring re-processing of notification??
if (oProposed.CompleteByDate != oCurrent.CompleteByDate || !SameTags)
{
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.WorkorderCompletedStatusOverdue);
//new has date?
if (oProposed.CompleteByDate != null)
{
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderCompletedStatusOverdue).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.ObjectHasAllSubscriptionTags(proposedObj.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.WorkorderCompletedStatusOverdue,
UserId = sub.UserId,
AyaType = proposedObj.AyaType,
ObjectId = proposedObj.Id,
NotifySubscriptionId = sub.Id,
Name = oProposed.Serial.ToString(),
EventDate = (DateTime)oProposed.CompleteByDate
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}
}
}//CREATED overdue completion
#endregion
#region CustomerServiceImminent
if (ayaEvent == AyaEvent.Created && oProposed.ServiceDate != null && oProposed.ServiceDate > DateTime.UtcNow)
{
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.CustomerServiceImminent).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//No tag match for this one
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.CustomerServiceImminent,
UserId = sub.UserId,
AyaType = proposedObj.AyaType,
ObjectId = proposedObj.Id,
NotifySubscriptionId = sub.Id,
Name = oProposed.Serial.ToString(),
EventDate = (DateTime)oProposed.ServiceDate
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}//CustomerServiceImminent
if (ayaEvent == AyaEvent.Modified)
{// CustomerServiceImminent
//differences requiring re-processing of notification
if (oProposed.ServiceDate != oCurrent.ServiceDate)
{
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.CustomerServiceImminent);
//new has date?
if (oProposed.ServiceDate != null && oProposed.ServiceDate > DateTime.UtcNow)
{
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.CustomerServiceImminent).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//No tag match for this one
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.CustomerServiceImminent,
UserId = sub.UserId,
AyaType = proposedObj.AyaType,
ObjectId = proposedObj.Id,
NotifySubscriptionId = sub.Id,
Name = oProposed.Serial.ToString(),
EventDate = (DateTime)oProposed.ServiceDate
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}
}//CustomerServiceImminent
#endregion
}//end of process notifications
#endregion workorder level
/*
███████╗████████╗ █████╗ ████████╗███████╗███████╗
██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██╔════╝██╔════╝
███████╗ ██║ ███████║ ██║ █████╗ ███████╗
╚════██║ ██║ ██╔══██║ ██║ ██╔══╝ ╚════██║
███████║ ██║ ██║ ██║ ██║ ███████╗███████║
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝
*/
#region WorkOrderState level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> StateExistsAsync(long id)
{
return await ct.WorkOrderState.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderState> StateCreateAsync(WorkOrderState newObject)
{
await StateValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.WorkOrderState.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderStatus, AyaEvent.Created), ct);
await StateHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderState> StateGetAsync(long id, bool logTheGetEvent = true)
{
//Note: there could be rules checking here in future, i.e. can only get own workorder or something
//if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now
//https://docs.microsoft.com/en-us/ef/core/querying/related-data
//docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections
var ret = await ct.WorkOrderState.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderStatus, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task StatePopulateVizFields(WorkOrderState o)
{
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
// (note: this would only ever be called when a workorder is deleted, there is no direct delete)
internal async Task<bool> StateDeleteAsync(long workOrderId, IDbContextTransaction parentTransaction)
{
try
{
var stateList = await ct.WorkOrderState.AsNoTracking().Where(z => z.WorkOrderId == workOrderId).ToListAsync();
foreach (var wostate in stateList)
{
ct.WorkOrderState.Remove(wostate);
await ct.SaveChangesAsync();
//no need to call this because it's only going to run this method if the workorder is deleted and
//via process standard notifciation events for workorder deletion will remove any state delayed notifications anyway so
//nothing to call or do here related to notification
// await StateHandlePotentialNotificationEvent(AyaEvent.Deleted, wostate);
}
}
catch
{
//Just re-throw for now, let exception handler deal, but in future may want to deal with this more here
throw;
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task StateValidateAsync(WorkOrderState proposedObj, WorkOrderState currentObj)
{
// //skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
//does it have a valid workorder id
if (proposedObj.WorkOrderId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId");
else if (!await WorkOrderExistsAsync(proposedObj.WorkOrderId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderId");
}
}
// private void StateValidateCanDelete(WorkOrderState obj)
// {
// if (obj == null)
// {
// AddError(ApiErrorCode.NOT_FOUND, "id");
// return;
// }
// //re-check rights here necessary due to traversal delete from Principle object
// if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderStatus))
// {
// AddError(ApiErrorCode.NOT_AUTHORIZED);
// return;
// }
// }
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task StateHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderState oProposed = (WorkOrderState)proposedObj;
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == oProposed.WorkOrderId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
WorkOrderStatus wos = await ct.WorkOrderStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.WorkOrderStatusId);
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
//NONE: state notifications are specific and not the same as for general objects so don't process standard events
//SPECIFIC EVENTS FOR THIS OBJECT
//WorkorderStatusChange = 4,//*Workorder object, any NEW status set. Conditions: specific status ID value only (no generic any status allowed), Workorder TAGS
//WorkorderCompletedStatusOverdue = 15,//* Workorder object not set to a "Completed" flagged workorder status type in selected time span from creation of workorderWorkorderSetToCompletedStatus
//WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set
//NOTE: ID, state notifications are for the Workorder, not the state itself unlike other objects, so use the WO type and ID here for all notifications
//## DELETED EVENTS
//A state cannot be deleted so nothing to handle that is required
//a workorder CAN be deleted and it will automatically remove all events for it so also no need to remove time delayed status events either if wo is deleted.
//so in essence there is nothing to be done regarding deleted events with states in a blanket way, however specific events below may remove them as appropriate
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusChange);
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderCompletedStatusOverdue);
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.WorkorderStatusAge);
//## CREATED (this is the only possible notification CREATION ayaEvent type for a workorder state as they are create only)
if (ayaEvent == AyaEvent.Created)
{
//# STATUS CHANGE (create new status)
{
//Conditions: must match specific status id value and also tags below
//delivery is immediate so no need to remove old ones of this kind
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusChange && z.IdValue == oProposed.WorkOrderStatusId).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.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.WorkorderStatusChange,
UserId = sub.UserId,
AyaType = AyaType.WorkOrder,
ObjectId = oProposed.WorkOrderId,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//workorder status change event
//# STATUS AGE
{
//WorkorderStatusAge = 24,//* Workorder STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set
//Always clear any old ones for this object as they are all irrelevant the moment the state has changed:
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.WorkorderStatusAge);
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderStatusAge && z.IdValue == oProposed.WorkOrderStatusId).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//WorkOrder Tag match? (Not State, state has no tags, will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.WorkorderStatusAge,
UserId = sub.UserId,
AyaType = AyaType.WorkOrder,
ObjectId = oProposed.WorkOrderId,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()} - {wos.Name}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//workorder status change event
//# COMPLETE BY OVERDUE
{
//NOTE: the initial notification is created by the Workorder Header notification as it's where this time delayed notification is first generated
//the only job here in state notification is to remove any prior finish overdue notifications waiting if a new state is selected that is a completed state
//NOTE ABOUT RE-OPEN DECISION ON HOW THIS WORKS:
//what though if it's not a Completed status, then I guess don't remove it, but what if it *was* a Completed status and it's change to a non Completed?
//that, in essence re-opens it so it's not Completed at that point.
//My decision on this june 2021 is that a work order Completed status notification is satisifed the moment it's saved with a Completed status
//and nothing afterwards restarts that process so if a person sets closed status then sets open status again no new Completed overdue notification will be generated
if (wos.Completed)
{
//Workorder was just set to a completed status so remove any notify events lurking to deliver for overdue
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, oProposed.WorkOrderId, NotifyEventType.WorkorderCompletedStatusOverdue);
}
}//workorder complete by overdue change event
//# WorkorderTotalExceedsThreshold / "The Andy"
{
if (wos.Completed)
{
//see if any subscribers to the workorder total exceeds notification
//that are active then proceed to fetch billed woitem children and total workorder and send notification if necessary
bool haveTotal = false;
decimal GrandTotal = 0m;
//look for potential subscribers
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.WorkorderTotalExceedsThreshold).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)
//check early to avoid cost of fetching and calculating total if unnecessary
if (!NotifyEventHelper.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags)) continue;
//get the total because we have at least one subscriber and matching tags
if (haveTotal == false)
{
GrandTotal = await WorkorderGrandTotalAsync(oProposed.WorkOrderId, ct);
haveTotal = true;
//Note: not a time delayed notification, however user could be flipping states quickly triggering multiple notifications that are in queue temporarily
//so this will prevent that:
await NotifyEventHelper.ClearPriorEventsForObject(ct, AyaType.WorkOrder, oProposed.WorkOrderId, NotifyEventType.WorkorderTotalExceedsThreshold);
}
//Ok, we're here because there is a subscriber who is active and tags match so only check left is total against decvalue
if (sub.DecValue < GrandTotal)
{
//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();
}
}
}
}//"The Andy" for Dynamic Dental corp. notification
}
}//end of process notifications
#endregion work order STATE level
/*
██╗████████╗███████╗███╗ ███╗███████╗
██║╚══██╔══╝██╔════╝████╗ ████║██╔════╝
██║ ██║ █████╗ ██╔████╔██║███████╗
██║ ██║ ██╔══╝ ██║╚██╔╝██║╚════██║
██║ ██║ ███████╗██║ ╚═╝ ██║███████║
╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝
*/
#region WorkOrderItem level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ItemExistsAsync(long id)
{
return await ct.WorkOrderItem.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItem> ItemCreateAsync(WorkOrderItem newObject)
{
await ItemValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItem.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.WorkOrderItem, AyaEvent.Created), ct);
await ItemSearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await ItemHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await ItemPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItem> ItemGetAsync(long id, bool logTheGetEvent = true)
{
//Note: there could be rules checking here in future, i.e. can only get own workorder or something
//if so, then need to implement AddError and in route handle Null return with Error check just like PUT route does now
//https://docs.microsoft.com/en-us/ef/core/querying/related-data
//docs say this will not query twice but will recognize the duplicate woitem bit which is required for multiple grandchild collections
var ret =
await ct.WorkOrderItem.AsSplitQuery().AsNoTracking()
.Include(wi => wi.Expenses)
.Include(wi => wi.Labors)
.Include(wi => wi.Loans)
.Include(wi => wi.Parts)
.Include(wi => wi.PartRequests)
.Include(wi => wi.ScheduledUsers)
.Include(wi => wi.Tasks)
.Include(wi => wi.Travels)
.Include(wi => wi.Units)
.Include(wi => wi.OutsideServices)
.SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItem, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItem> ItemPutAsync(WorkOrderItem putObject)
{
//Note: this is intentionally not using the getasync because
//doing so would also fetch the children which would then get deleted on save since putobject has no children
var dbObject = await ct.WorkOrderItem.AsNoTracking().FirstOrDefaultAsync(z => z.Id == putObject.Id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
putObject.Tags = TagBiz.NormalizeTags(putObject.Tags);
putObject.CustomFields = JsonUtil.CompactJson(putObject.CustomFields);
await ItemValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await ItemExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, AyaType.WorkOrderItem, AyaEvent.Modified), ct);
await ItemSearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await ItemHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await ItemPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ItemDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await ct.WorkOrderItem.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
ItemValidateCanDelete(dbObject);
if (HasErrors)
return false;
//collect the child id's to delete
var ExpenseIds = await ct.WorkOrderItemExpense.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var LaborIds = await ct.WorkOrderItemLabor.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var LoanIds = await ct.WorkOrderItemLoan.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var PartIds = await ct.WorkOrderItemPart.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var PartRequestIds = await ct.WorkOrderItemPartRequest.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var ScheduledUserIds = await ct.WorkOrderItemScheduledUser.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var TaskIds = await ct.WorkOrderItemTask.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var TravelIds = await ct.WorkOrderItemTravel.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var UnitIds = await ct.WorkOrderItemUnit.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
var OutsideServiceIds = await ct.WorkOrderItemOutsideService.Where(z => z.WorkOrderItemId == id).Select(z => z.Id).ToListAsync();
//Delete children
foreach (long ItemId in ExpenseIds)
if (!await ExpenseDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in LaborIds)
if (!await LaborDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in LoanIds)
if (!await LoanDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in PartIds)
if (!await PartDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in PartRequestIds)
if (!await PartRequestDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in ScheduledUserIds)
if (!await ScheduledUserDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in TaskIds)
if (!await TaskDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in TravelIds)
if (!await TravelDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in UnitIds)
if (!await UnitDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in OutsideServiceIds)
if (!await OutsideServiceDeleteAsync(ItemId, transaction))
return false;
ct.WorkOrderItem.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "wo:" + dbObject.WorkOrderId.ToString(), ct);//FIX wo?? Not sure what is best here; revisit
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
//all good do the commit if it's ours
if (parentTransaction == null)
await transaction.CommitAsync();
await ItemHandlePotentialNotificationEvent(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;
}
private async Task ItemSearchIndexAsync(WorkOrderItem obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.WorkOrderItem);
SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> ItemGetSearchResultSummary(long id)
{
var obj = await ct.WorkOrderItem.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);//# Note, intentionally not calling ItemGetAsync here as don't want whole graph
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Notes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task ItemPopulateVizFields(WorkOrderItem o)
{
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
foreach (var v in o.Expenses)
await ExpensePopulateVizFields(v);
foreach (var v in o.Labors)
await LaborPopulateVizFields(v);
foreach (var v in o.Loans)
await LoanPopulateVizFields(v);
foreach (var v in o.OutsideServices)
await OutsideServicePopulateVizFields(v);
foreach (var v in o.PartRequests)
await PartRequestPopulateVizFields(v);
foreach (var v in o.Parts)
await PartPopulateVizFields(v);
foreach (var v in o.ScheduledUsers)
await ScheduledUserPopulateVizFields(v);
foreach (var v in o.Tasks)
await TaskPopulateVizFields(v);
foreach (var v in o.Travels)
await TravelPopulateVizFields(v);
foreach (var v in o.Units)
await UnitPopulateVizFields(v);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ItemValidateAsync(WorkOrderItem proposedObj, WorkOrderItem currentObj)
{
//run validation and biz rules
bool isNew = currentObj == null;
//does it have a valid workorder id
if (proposedObj.WorkOrderId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId");
else if (!await WorkOrderExistsAsync(proposedObj.WorkOrderId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderId");
}
//summary is required now, this is a change from v7
//I did this because it is required in terms of hiding on the form so it also
//is required to have a value. This is really because the form field customization I took away the hideable field
//maybe I should add that feature back?
if (proposedObj.WorkOrderId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderId");
// // //TEST TEST TEST
// if (string.IsNullOrWhiteSpace(proposedObj.Notes))
// {
// AddError(ApiErrorCode.VALIDATION_REQUIRED, "Notes");
// }
// if (proposedObj.Notes.Contains("blah"))
// {
// ;
// }
// if (proposedObj.Notes != null && proposedObj.Notes.Contains("generalerror"))
// {
// AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", "Test general error");
// }
// if (proposedObj.Notes != null && proposedObj.Notes.Contains("aytesterror"))
// {
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Notes", "SAVE TEST ERROR");
// }
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItem.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);//note: this is passed only to add errors
//validate custom fields
CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void ItemValidateCanDelete(WorkOrderItem obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
// //TEST TEST TEST
// if (obj.Notes != null && obj.Notes.Contains("aytesterror"))
// {
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, $"Notes", "DELETE TEST ERROR");
// }
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItem))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task ItemHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItem oProposed = (WorkOrderItem)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
//## DELETED EVENTS
//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
// await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, o.Id, NotifyEventType.ContractExpiring);
//## CREATED / MODIFIED EVENTS
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
{
//todo: fix etc, tons of shit here incoming
}
}//end of process notifications
#endregion work order item level
/*
███████╗██╗ ██╗██████╗ ███████╗███╗ ██╗███████╗███████╗███████╗
██╔════╝╚██╗██╔╝██╔══██╗██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝
█████╗ ╚███╔╝ ██████╔╝█████╗ ██╔██╗ ██║███████╗█████╗ ███████╗
██╔══╝ ██╔██╗ ██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══╝ ╚════██║
███████╗██╔╝ ██╗██║ ███████╗██║ ╚████║███████║███████╗███████║
╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝
*/
#region WorkOrderItemExpense level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExpenseExistsAsync(long id)
{
return await ct.WorkOrderItemExpense.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemExpense> ExpenseCreateAsync(WorkOrderItemExpense newObject)
{
await ExpenseValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
//await ExpenseBizActionsAsync(AyaEvent.Created, newObject, null, null);
// newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
// newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemExpense.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await ExpenseSearchIndexAsync(newObject, true);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await ExpensePopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemExpense> ExpenseGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemExpense.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemExpense> ExpensePutAsync(WorkOrderItemExpense putObject)
{
var dbObject = await ExpenseGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
// dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await ExpenseValidateAsync(putObject, dbObject);
if (HasErrors) return null;
//await ExpenseBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await ExpenseExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await ExpenseSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await ExpensePopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ExpenseDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await ExpenseGetAsync(id, false);
ExpenseValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemExpense.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await ExpenseHandlePotentialNotificationEvent(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;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task ExpenseSearchIndexAsync(WorkOrderItemExpense obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.Name).AddText(obj.Description);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> ExpenseGetSearchResultSummary(long id)
{
var obj = await ExpenseGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Description).AddText(obj.Name);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task ExpensePopulateVizFields(WorkOrderItemExpense o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.UserId != null)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
}
TaxCode Tax = null;
if (o.ChargeTaxCodeId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ChargeTaxCodeId);
if (Tax != null)
o.ChargeTaxCodeViz = Tax.Name;
//Calculate totals and taxes
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.ChargeAmount * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.ChargeAmount + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.ChargeAmount * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.ChargeAmount + o.TaxAViz + o.TaxBViz;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //BIZ ACTIONS
// //
// //
// private async Task ExpenseBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemExpense newObj, WorkOrderItemExpense oldObj, IDbContextTransaction transaction)
// {
// //automatic actions on record change, called AFTER validation
// //currently no processing required except for created or modified at this time
// if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
// return;
// //SET TAXES AND PRICING
// //by default apply all automatic actions with further restrictions possible below
// bool ApplyTax = true;
// //if modifed, see what has changed and should be re-applied
// if (ayaEvent == AyaEvent.Modified)
// {
// //If taxes haven't change then no need to update taxes
// if (newObj.ChargeTaxCodeId == oldObj.ChargeTaxCodeId)
// ApplyTax = false;
// }
// //Tax code
// if (ApplyTax)
// {
// //Default in case nothing to apply
// newObj.TaxAPct = 0;
// newObj.TaxBPct = 0;
// newObj.TaxOnTax = false;
// newObj.TaxName = "";
// if (newObj.ChargeTaxCodeId != null)
// {
// var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.ChargeTaxCodeId);
// if (t != null)
// {
// newObj.TaxAPct = t.TaxAPct;
// newObj.TaxBPct = t.TaxBPct;
// newObj.TaxOnTax = t.TaxOnTax;
// newObj.TaxName = t.Name;
// }
// }
// }
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ExpenseValidateAsync(WorkOrderItemExpense proposedObj, WorkOrderItemExpense currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemExpense.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);//note: this is passed only to add errors
//validate custom fields
// CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void ExpenseValidateCanDelete(WorkOrderItemExpense obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemExpense))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task ExpenseHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemExpense oProposed = (WorkOrderItemExpense)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item EXPENSE level
/*
██╗ █████╗ ██████╗ ██████╗ ██████╗
██║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗
██║ ███████║██████╔╝██║ ██║██████╔╝
██║ ██╔══██║██╔══██╗██║ ██║██╔══██╗
███████╗██║ ██║██████╔╝╚██████╔╝██║ ██║
╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝
*/
#region WorkOrderItemLabor level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> LaborExistsAsync(long id)
{
return await ct.WorkOrderItemLabor.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemLabor> LaborCreateAsync(WorkOrderItemLabor newObject)
{
await LaborValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
// await LaborBizActionsAsync(AyaEvent.Created, newObject, null, null);
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemLabor.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await LaborSearchIndexAsync(newObject, true);
// await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await LaborHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await LaborPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemLabor> LaborGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemLabor.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemLabor> LaborPutAsync(WorkOrderItemLabor putObject)
{
var dbObject = await LaborGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await LaborValidateAsync(putObject, dbObject);
if (HasErrors) return null;
// await LaborBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await LaborExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await LaborSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await LaborHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await LaborPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> LaborDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await LaborGetAsync(id, false);
LaborValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemLabor.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
// await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await LaborHandlePotentialNotificationEvent(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;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task LaborSearchIndexAsync(WorkOrderItemLabor obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.ServiceDetails);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> LaborGetSearchResultSummary(long id)
{
var obj = await LaborGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.ServiceDetails);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task LaborPopulateVizFields(WorkOrderItemLabor o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.UserId != null)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
}
ServiceRate Rate = null;
if (o.ServiceRateId != null)
{
Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.ServiceRateId);
o.ServiceRateViz = Rate.Name;
}
TaxCode Tax = null;
if (o.TaxCodeSaleId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
if (Tax != null)
o.TaxCodeSaleViz = Tax.Name;
o.PriceViz = 0;
if (Rate != null)
{
o.CostViz = Rate.Cost;
o.ListPriceViz = Rate.Charge;
o.UnitOfMeasureViz = Rate.Unit;
o.PriceViz = Rate.Charge;//default price used if not manual or contract override
}
//manual price overrides anything
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
else
{
//not manual so could potentially have a contract adjustment
var c = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId);
if (c != null)
{
decimal pct = 0;
ContractOverrideType cot = ContractOverrideType.PriceDiscount;
bool TaggedAdjustmentInEffect = false;
//POTENTIAL CONTRACT ADJUSTMENTS
//First check if there is a matching tagged service rate contract discount, that takes precedence
if (c.ContractServiceRateOverrideItems.Count > 0)
{
//Iterate all contract tagged items in order of ones with the most tags first
foreach (var csr in c.ContractServiceRateOverrideItems.OrderByDescending(z => z.Tags.Count))
if (csr.Tags.All(z => Rate.Tags.Any(x => x == z)))
{
if (csr.OverridePct != 0)
{
pct = csr.OverridePct / 100;
cot = csr.OverrideType;
TaggedAdjustmentInEffect = true;
}
}
}
//Generic discount?
if (!TaggedAdjustmentInEffect && c.ServiceRatesOverridePct != 0)
{
pct = c.ServiceRatesOverridePct / 100;
cot = c.ServiceRatesOverrideType;
}
//apply if discount found
if (pct != 0)
{
if (cot == ContractOverrideType.CostMarkup)
o.PriceViz = o.CostViz + (o.CostViz * pct);
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = o.ListPriceViz - (o.ListPriceViz * pct);
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.ServiceRateQuantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //BIZ ACTIONS
// //
// //
// private async Task LaborBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemLabor newObj, WorkOrderItemLabor oldObj, IDbContextTransaction transaction)
// {
// //automatic actions on record change, called AFTER validation
// //currently no processing required except for created or modified at this time
// if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
// return;
// //SET TAXES AND PRICING
// //by default apply all automatic actions with further restrictions possible below
// bool ApplyTax = true;
// bool SetPrice = true;
// //if modifed, see what has changed and should be re-applied
// if (ayaEvent == AyaEvent.Modified)
// {
// //If it wasn't a service rate or quantity change there is no need to set pricing
// if (newObj.ServiceRateId == oldObj.ServiceRateId && newObj.ServiceRateQuantity == oldObj.ServiceRateQuantity)
// {
// SetPrice = false;
// }
// //If taxes haven't change then no need to update taxes
// if (newObj.TaxCodeSaleId == oldObj.TaxCodeSaleId)
// ApplyTax = false;
// }
// //Tax code
// if (ApplyTax)
// {
// //Default in case nothing to apply
// newObj.TaxAPct = 0;
// newObj.TaxBPct = 0;
// newObj.TaxOnTax = false;
// newObj.TaxName = "";
// if (newObj.TaxCodeSaleId != null)
// {
// var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TaxCodeSaleId);
// if (t != null)
// {
// newObj.TaxAPct = t.TaxAPct;
// newObj.TaxBPct = t.TaxBPct;
// newObj.TaxOnTax = t.TaxOnTax;
// newObj.TaxName = t.Name;
// }
// }
// }
// //Pricing
// if (SetPrice)
// {
// var Contract = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, newObj.WorkOrderItemId);
// await LaborSetPrice(newObj, Contract);
// }
// }
// ////////////////////////////////////////////////////////////////////////////////////////////////
// // SET PER UNIT LIST PRICE
// //
// //(called by woitemlabor save and also by header save on change of contract)
// private async Task LaborSetPrice(WorkOrderItemLabor o, Contract c)
// {
// //default in case nothing to apply
// o.Cost = 0;
// o.ListPrice = 0;
// o.Price = 0;
// //in v7 it was ok to have no service rate selected
// //not sure why but carried forward to v8 so..
// if (o.ServiceRateId == null)
// return;
// var Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ServiceRateId);
// if (Rate == null)
// {
// AddError(ApiErrorCode.NOT_FOUND, "generalerror", "Service rate not found");//this should never happen, no point in localizing
// return;
// }
// o.Cost = Rate.Cost;
// o.ListPrice = Rate.Charge;
// o.Price = o.ListPrice;//default is list price unless a contract overrides it
// if (c == null)
// return;//No contract so bail out now, it's done
// //POTENTIAL CONTRACT ADJUSTMENTS
// //First check if there is a matching tagged service rate contract discount, that takes precedence
// if (c.ContractServiceRateOverrideItems.Count > 0)
// {
// //Iterate all contract tagged items in order of ones with the most tags first
// foreach (var csr in c.ContractServiceRateOverrideItems.OrderByDescending(z => z.Tags.Count))
// if (csr.Tags.All(z => Rate.Tags.Any(x => x == z)))
// {
// if (csr.OverridePct != 0)
// {
// var pct = csr.OverridePct / 100;
// //found a match, apply the discount and return
// if (csr.OverrideType == ContractOverrideType.CostMarkup)
// o.Price = o.Cost + (o.Cost * pct);
// else if (csr.OverrideType == ContractOverrideType.PriceDiscount)
// o.Price = o.ListPrice - (o.ListPrice * pct);
// return;
// }
// }
// }
// //No tag discounts, so check for a generic one
// if (c.ServiceRatesOverridePct == 0)
// return;// no generic discount for all items so bail now
// {
// var pct = c.ServiceRatesOverridePct / 100;
// //Contract has a generic override so apply it
// if (c.ServiceRatesOverrideType == ContractOverrideType.CostMarkup)
// o.Price = o.Cost + (o.Cost * pct);
// else if (c.ServiceRatesOverrideType == ContractOverrideType.PriceDiscount)
// o.Price = o.ListPrice - (o.ListPrice * pct);
// }
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task LaborValidateAsync(WorkOrderItemLabor proposedObj, WorkOrderItemLabor currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemLabor.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);//note: this is passed only to add errors
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void LaborValidateCanDelete(WorkOrderItemLabor obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemLabor))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task LaborHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemLabor oProposed = (WorkOrderItemLabor)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item LABOR level
/*
██╗ ██████╗ █████╗ ███╗ ██╗
██║ ██╔═══██╗██╔══██╗████╗ ██║
██║ ██║ ██║███████║██╔██╗ ██║
██║ ██║ ██║██╔══██║██║╚██╗██║
███████╗╚██████╔╝██║ ██║██║ ╚████║
*/
#region WorkOrderItemLoan level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> LoanExistsAsync(long id)
{
return await ct.WorkOrderItemLoan.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemLoan> LoanCreateAsync(WorkOrderItemLoan newObject)
{
await LoanValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await LoanBizActionsAsync(AyaEvent.Created, newObject, null, null);
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemLoan.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await LoanSearchIndexAsync(newObject, true);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await LoanHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await LoanPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemLoan> LoanGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemLoan.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemLoan> LoanPutAsync(WorkOrderItemLoan putObject)
{
var dbObject = await LoanGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
// dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await LoanValidateAsync(putObject, dbObject);
if (HasErrors) return null;
await LoanBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await LoanExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await LoanSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await LoanHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await LoanPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> LoanDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await LoanGetAsync(id, false);
LoanValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemLoan.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await LoanHandlePotentialNotificationEvent(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;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task LoanSearchIndexAsync(WorkOrderItemLoan obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.Notes);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> LoanGetSearchResultSummary(long id)
{
var obj = await LoanGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Notes);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task LoanPopulateVizFields(WorkOrderItemLoan o, List<NameIdItem> loanUnitRateEnumList = null, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (loanUnitRateEnumList == null)
loanUnitRateEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList(
StringUtil.TrimTypeName(typeof(LoanUnitRateUnit).ToString()),
UserTranslationId,
CurrentUserRoles);
o.UnitOfMeasureViz = loanUnitRateEnumList.Where(x => x.Id == (long)o.Rate).Select(x => x.Name).First();
}
LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.LoanUnitId);
o.LoanUnitViz = loanUnit.Name;
TaxCode Tax = null;
if (o.TaxCodeId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId);
if (Tax != null)
o.TaxCodeViz = Tax.Name;
//manual price overrides anything
o.PriceViz = o.ListPrice;
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
//Currently not contract discounted so no further calcs need apply to priceViz
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.Quantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task LoanBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemLoan newObj, WorkOrderItemLoan oldObj, IDbContextTransaction transaction)
{
//automatic actions on record change, called AFTER validation
//currently no processing required except for created or modified at this time
if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
return;
//SNAPSHOT PRICING
bool SnapshotPricing = true;
//if modifed, see what has changed and should be re-applied
if (ayaEvent == AyaEvent.Modified)
{
//If it wasn't a complete part change there is no need to set pricing
if (newObj.LoanUnitId == oldObj.LoanUnitId && newObj.Rate == oldObj.Rate)
SnapshotPricing = false;
}
//Pricing
if (SnapshotPricing)
{
//default in case nothing to apply
newObj.Cost = 0;
newObj.ListPrice = 0;
LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == newObj.LoanUnitId);
if (loanUnit != null)
{
switch (newObj.Rate)
{
case LoanUnitRateUnit.None:
break;
case LoanUnitRateUnit.Hours:
newObj.Cost = loanUnit.RateHourCost;
newObj.ListPrice = loanUnit.RateHour;
break;
case LoanUnitRateUnit.HalfDays:
newObj.Cost = loanUnit.RateHalfDayCost;
newObj.ListPrice = loanUnit.RateHalfDay;
break;
case LoanUnitRateUnit.Days:
newObj.Cost = loanUnit.RateDayCost;
newObj.ListPrice = loanUnit.RateDay;
break;
case LoanUnitRateUnit.Weeks:
newObj.Cost = loanUnit.RateWeekCost;
newObj.ListPrice = loanUnit.RateWeek;
break;
case LoanUnitRateUnit.Months:
newObj.Cost = loanUnit.RateMonthCost;
newObj.ListPrice = loanUnit.RateMonth;
break;
case LoanUnitRateUnit.Years:
newObj.Cost = loanUnit.RateYearCost;
newObj.ListPrice = loanUnit.RateYear;
break;
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task LoanValidateAsync(WorkOrderItemLoan proposedObj, WorkOrderItemLoan currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (proposedObj.LoanUnitId < 1 || !await ct.LoanUnit.AnyAsync(x => x.Id == proposedObj.LoanUnitId))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "LoanUnitId");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemLoan.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);//note: this is passed only to add errors
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void LoanValidateCanDelete(WorkOrderItemLoan obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemLoan))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task LoanHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemLoan oProposed = (WorkOrderItemLoan)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item LOAN level
/*
██████╗ ██╗ ██╗████████╗███████╗██╗██████╗ ███████╗ ███████╗███████╗██████╗ ██╗ ██╗██╗ ██████╗███████╗
██╔═══██╗██║ ██║╚══██╔══╝██╔════╝██║██╔══██╗██╔════╝ ██╔════╝██╔════╝██╔══██╗██║ ██║██║██╔════╝██╔════╝
██║ ██║██║ ██║ ██║ ███████╗██║██║ ██║█████╗ ███████╗█████╗ ██████╔╝██║ ██║██║██║ █████╗
██║ ██║██║ ██║ ██║ ╚════██║██║██║ ██║██╔══╝ ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██║██║ ██╔══╝
╚██████╔╝╚██████╔╝ ██║ ███████║██║██████╔╝███████╗ ███████║███████╗██║ ██║ ╚████╔╝ ██║╚██████╗███████╗
╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝╚══════╝
*/
#region WorkOrderItemOutsideService level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> OutsideServiceExistsAsync(long id)
{
return await ct.WorkOrderItemOutsideService.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemOutsideService> OutsideServiceCreateAsync(WorkOrderItemOutsideService newObject)
{
await OutsideServiceValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
// newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemOutsideService.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await OutsideServiceSearchIndexAsync(newObject, true);
// await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await OutsideServicePopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemOutsideService> OutsideServiceGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemOutsideService.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.WorkOrderItemOutsideService, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemOutsideService> OutsideServicePutAsync(WorkOrderItemOutsideService putObject)
{
WorkOrderItemOutsideService dbObject = await OutsideServiceGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
// dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await OutsideServiceValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await OutsideServiceExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await OutsideServiceSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await OutsideServicePopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> OutsideServiceDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await OutsideServiceGetAsync(id, false);
OutsideServiceValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemOutsideService.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await OutsideServiceHandlePotentialNotificationEvent(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;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task OutsideServiceSearchIndexAsync(WorkOrderItemOutsideService obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.Notes).AddText(obj.RMANumber).AddText(obj.TrackingNumber);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> OutsideServiceGetSearchResultSummary(long id)
{
var obj = await OutsideServiceGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Notes).AddText(obj.RMANumber).AddText(obj.TrackingNumber);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task OutsideServicePopulateVizFields(WorkOrderItemOutsideService o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.UnitId != 0)
o.UnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync();
if (o.VendorSentToId != null)
o.VendorSentToViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentToId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.VendorSentViaId != null)
o.VendorSentViaViz = await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentViaId).Select(x => x.Name).FirstOrDefaultAsync();
}
TaxCode Tax = null;
if (o.TaxCodeId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId);
if (Tax != null)
o.TaxCodeViz = Tax.Name;
o.CostViz = o.ShippingCost + o.RepairCost;
o.PriceViz = o.ShippingPrice + o.RepairPrice;
//Currently not contract discounted so no further calcs need apply to priceViz
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz;//just for standardization, no quantity so is redundant but reporting easier if all the same
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task OutsideServiceValidateAsync(WorkOrderItemOutsideService proposedObj, WorkOrderItemOutsideService currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (proposedObj.UnitId < 1 || !await ct.Unit.AnyAsync(x => x.Id == proposedObj.UnitId))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemOutsideService.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);//note: this is passed only to add errors
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void OutsideServiceValidateCanDelete(WorkOrderItemOutsideService obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemOutsideService))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task OutsideServiceHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
/*
OutsideServiceOverdue = 16,//* Workorder object , WorkorderItemOutsideService created / updated, sets advance notice on due date tag filterable
OutsideServiceReceived = 17,//* Workorder object , WorkorderItemOutsideService updated, instant notification when item received, tag filterable
*/
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemOutsideService oProposed = (WorkOrderItemOutsideService)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
//## DELETED EVENTS
//standard process above will remove any hanging around when deleted, nothing else specific here to deal with
//## CREATED
if (ayaEvent == AyaEvent.Created)
{
//OutsideServiceOverdue
if (oProposed.ETADate != null)
{
//Conditions: tags + time delayed eta value
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceOverdue).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.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.OutsideServiceOverdue,
UserId = sub.UserId,
AyaType = AyaType.WorkOrderItemOutsideService,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
EventDate = (DateTime)oProposed.ETADate,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//OutsideServiceOverdue
//OutsideServiceReceived (here because it's possible a outside service is entered new with both an eta and received date if entered after the fact)
if (oProposed.ReturnDate != null)
{
//Clear overdue ones as it's now received
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
//Conditions: tags
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceReceived).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.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.OutsideServiceReceived,
UserId = sub.UserId,
AyaType = AyaType.WorkOrderItemOutsideService,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//OutsideServiceReceived
}
//## MODIFIED
if (ayaEvent == AyaEvent.Modified)
{
WorkOrderItemOutsideService oCurrent = (WorkOrderItemOutsideService)currentObj;
//OutsideServiceOverdue
//if modified then remove any potential prior ones in case irrelevant
if (oProposed.ETADate != oCurrent.ETADate)
{
//eta changed, so first of all remove any prior ones
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
//now can go ahead and add back again as appropriate
if (oProposed.ETADate != null)
{
//Conditions: tags + time delayed eta value
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceOverdue).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.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.OutsideServiceOverdue,
UserId = sub.UserId,
AyaType = AyaType.WorkOrderItemOutsideService,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
EventDate = (DateTime)oProposed.ETADate,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//OutsideServiceOverdue
}
//OutsideServiceReceived
if (oProposed.ReturnDate != oCurrent.ReturnDate && oProposed.ReturnDate != null)//note that this is an instant notification type so no need to clear older ones like above which is time delayed
{
//Clear overdue ones as it's now received
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.OutsideServiceOverdue);
//Conditions: tags
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.OutsideServiceReceived).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.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.OutsideServiceReceived,
UserId = sub.UserId,
AyaType = AyaType.WorkOrderItemOutsideService,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//OutsideServiceReceived
}
}//end of process notifications
#endregion work order item OUTSIDE SERVICE level
/*
██████╗ █████╗ ██████╗ ████████╗███████╗
██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
██████╔╝███████║██████╔╝ ██║ ███████╗
██╔═══╝ ██╔══██║██╔══██╗ ██║ ╚════██║
██║ ██║ ██║██║ ██║ ██║ ███████║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
*/
#region WorkOrderItemPart level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> PartExistsAsync(long id)
{
return await ct.WorkOrderItemPart.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemPart> CreatePartAsync(WorkOrderItemPart newObject)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
await PartValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await PartBizActionsAsync(AyaEvent.Created, newObject, null, null);
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemPart.AddAsync(newObject);
await ct.SaveChangesAsync();
await PartInventoryAdjustmentAsync(AyaEvent.Created, newObject, null, transaction);
if (HasErrors)
{
await transaction.RollbackAsync();
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await PartSearchIndexAsync(newObject, true);
await transaction.CommitAsync();
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await PartPopulateVizFields(newObject);
return newObject;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemPart> PartGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemPart.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemPart> PartPutAsync(WorkOrderItemPart putObject)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
WorkOrderItemPart dbObject = await PartGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await PartValidateAsync(putObject, dbObject);
if (HasErrors) return null;
await PartBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
await PartInventoryAdjustmentAsync(AyaEvent.Modified, putObject, dbObject, transaction);
if (HasErrors)
{
await transaction.RollbackAsync();
return null;
}
}
catch (DbUpdateConcurrencyException)
{
if (!await PartExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await PartSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await PartHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await transaction.CommitAsync();
await PartPopulateVizFields(putObject);
return putObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> PartDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
// var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
using (var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync())
{
try
{
var dbObject = await PartGetAsync(id, false);
PartValidateCanDelete(dbObject);
if (HasErrors)
return false;
await PartBizActionsAsync(AyaEvent.Deleted, null, dbObject, transaction);
ct.WorkOrderItemPart.Remove(dbObject);
await ct.SaveChangesAsync();
await PartInventoryAdjustmentAsync(AyaEvent.Deleted, null, dbObject, transaction);
if (HasErrors)
{
await transaction.RollbackAsync();
return false;
}
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await PartHandlePotentialNotificationEvent(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;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task PartSearchIndexAsync(WorkOrderItemPart obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.Description).AddText(obj.Serials);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> PartGetSearchResultSummary(long id)
{
var obj = await PartGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Description).AddText(obj.Serials);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task PartPopulateVizFields(WorkOrderItemPart o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.PartWarehouseId != 0)
o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync();
}
Part part = null;
if (o.PartId != 0)
part = await ct.Part.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.PartId);
else
return;//this should never happen but this is insurance in case it does
o.PartViz = part.PartNumber;
o.UpcViz = part.UPC;
TaxCode Tax = null;
if (o.TaxPartSaleId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxPartSaleId);
if (Tax != null)
o.TaxPartSaleViz = Tax.Name;
o.PriceViz = 0;
if (part != null)
{
// o.CostViz = part.Cost;
// o.ListPriceViz = part.Retail;
o.UnitOfMeasureViz = part.UnitOfMeasure;
o.PriceViz = o.ListPrice;//default price used if not manual or contract override
}
//manual price overrides anything
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
else
{
//not manual so could potentially have a contract adjustment
var c = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId);
if (c != null)
{
decimal pct = 0;
ContractOverrideType cot = ContractOverrideType.PriceDiscount;
bool TaggedAdjustmentInEffect = false;
//POTENTIAL CONTRACT ADJUSTMENTS
//First check if there is a matching tagged contract discount, that takes precedence
if (c.ContractPartOverrideItems.Count > 0)
{
//Iterate all contract tagged items in order of ones with the most tags first
foreach (var cp in c.ContractPartOverrideItems.OrderByDescending(z => z.Tags.Count))
if (cp.Tags.All(z => part.Tags.Any(x => x == z)))
{
if (cp.OverridePct != 0)
{
pct = cp.OverridePct / 100;
cot = cp.OverrideType;
TaggedAdjustmentInEffect = true;
}
}
}
//Generic discount?
if (!TaggedAdjustmentInEffect && c.ServiceRatesOverridePct != 0)
{
pct = c.ServiceRatesOverridePct / 100;
cot = c.ServiceRatesOverrideType;
}
//apply if discount found
if (pct != 0)
{
if (cot == ContractOverrideType.CostMarkup)
o.PriceViz = o.Cost + (o.Cost * pct);
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = o.ListPrice - (o.ListPrice * pct);
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.Quantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task PartBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart oldObj, IDbContextTransaction transaction)
{
//SNAPSHOT PRICING IF NECESSARY
if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
return;
//SNAPSHOT PRICING
bool SnapshotPricing = true;
//if modifed, see what has changed and should be re-applied
if (ayaEvent == AyaEvent.Modified)
{
//If it wasn't a complete part change there is no need to set pricing
if (newObj.PartId == oldObj.PartId)
{
SnapshotPricing = false;
}
}
//Pricing
if (SnapshotPricing)
{
//default in case nothing to apply
newObj.Cost = 0;
newObj.ListPrice = 0;
var s = await ct.Part.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.PartId);
if (s != null)
{
newObj.Cost = s.Cost;
newObj.ListPrice = s.Retail;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task PartInventoryAdjustmentAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart oldObj, IDbContextTransaction transaction)
{
if (AyaNova.Util.ServerGlobalBizSettings.UseInventory)
{
PartInventoryBiz pib = new PartInventoryBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
//DELETED, HANDLE INVENTORY / RETURN SERIALS
if (ayaEvent == AyaEvent.Deleted)
{
dtInternalPartInventory pi =
new dtInternalPartInventory
{
PartId = oldObj.PartId,
PartWarehouseId = oldObj.PartWarehouseId,
Quantity = oldObj.Quantity,
SourceType = null,//null because the po no longer exists so this is technically a manual adjustment
SourceId = null,//''
Description = await Translate("WorkOrderItemPart") + $" {oldObj.Serials} " + await Translate("EventDeleted")
};
if (await pib.CreateAsync(pi) == null)
{
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
return;
}
else
{ //return serial numbers to part
if (!string.IsNullOrWhiteSpace(oldObj.Serials))
await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
}
}
//CREATED, HANDLE INVENTORY / CONSUME SERIALS
if (ayaEvent == AyaEvent.Created && newObj.Quantity != 0)//allow zero quantity parts on workorder as placeholder, serials will not be consumed
{
dtInternalPartInventory pi =
new dtInternalPartInventory
{
PartId = newObj.PartId,
PartWarehouseId = newObj.PartWarehouseId,
Quantity = newObj.Quantity * -1,
SourceType = AyaType.WorkOrderItemPart,
SourceId = newObj.Id,
Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated")
};
if (await pib.CreateAsync(pi) == null)
{
if (pib.HasErrors)
{
foreach (var e in pib.Errors)
{
if (e.Code == ApiErrorCode.INSUFFICIENT_INVENTORY)
AddError(e.Code, "Quantity", e.Message);
else
AddError(e.Code, e.Target, e.Message);
}
}
//AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({pi.Description}):{pib.GetErrorsAsString()}");
return;
}
else
{ //Consume serial numbers from part
if (!string.IsNullOrWhiteSpace(newObj.Serials))
await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
}
}
//UPDATED, HANDLE INVENTORY / UPDATE SERIALS AS REQUIRED
if (ayaEvent == AyaEvent.Modified)
{
//INVENTORY
if (newObj.PartId != oldObj.PartId || newObj.Quantity != oldObj.Quantity)
{
//OUT with the old
if (oldObj.Quantity != 0)//zero quantity doesn't affect inventory or serials
{
dtInternalPartInventory piOld = new dtInternalPartInventory
{
PartId = oldObj.PartId,
PartWarehouseId = oldObj.PartWarehouseId,
Quantity = oldObj.Quantity,
SourceType = null,//null because the po no longer exists so this is technically a manual adjustment
SourceId = null,//''
Description = await Translate("WorkOrderItemPart") + $" {oldObj.Serials} " + await Translate("EventDeleted")
};
if (await pib.CreateAsync(piOld) == null)
{
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({piOld.Description}):{pib.GetErrorsAsString()}");
return;
}
else
{ //return serial numbers to part
if (!string.IsNullOrWhiteSpace(oldObj.Serials))
await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
}
}
//IN with the new
if (newObj.Quantity != 0)
{//NOTE: zero quantity is considered to be a placeholder and no serials will be consumed, nor inventory affected
dtInternalPartInventory piNew = new dtInternalPartInventory
{
PartId = newObj.PartId,
PartWarehouseId = newObj.PartWarehouseId,
Quantity = newObj.Quantity * -1,
SourceType = AyaType.WorkOrderItemPart,
SourceId = newObj.Id,
Description = await Translate("WorkOrderItemPart") + $" {newObj.Serials} " + await Translate("EventCreated")
};
if (await pib.CreateAsync(piNew) == null)
{
AddError(ApiErrorCode.API_SERVER_ERROR, "generalerror", $"Error updating inventory ({piNew.Description}):{pib.GetErrorsAsString()}");
return;
}
else
{ //Consume serial numbers from part
if (!string.IsNullOrWhiteSpace(newObj.Serials))
await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
}
}
}
//SERIALS
if (newObj.Serials != oldObj.Serials)
{
//NOTE: zero quantity is considered to be a placeholder and no serials will be consumed (hence not returned either)
//return serial numbers to part
if (oldObj.Quantity != 0 && !string.IsNullOrWhiteSpace(oldObj.Serials))
await PartBiz.AppendSerialsAsync(oldObj.PartId, oldObj.Serials, ct, UserId);
//Consume serial numbers from part
if (newObj.Quantity != 0 && !string.IsNullOrWhiteSpace(newObj.Serials))
await PartBiz.RemoveSerialsAsync(newObj.PartId, newObj.Serials, ct, UserId);
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task PartValidateAsync(WorkOrderItemPart proposedObj, WorkOrderItemPart currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemPart.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);//note: this is passed only to add errors
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void PartValidateCanDelete(WorkOrderItemPart obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemPart))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task PartHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemPart oProposed = (WorkOrderItemPart)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item PARTS level
/*
██████╗ █████╗ ██████╗ ████████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗███████╗███████╗████████╗███████╗
██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝ ██╔══██╗██╔════╝██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝██╔════╝
██████╔╝███████║██████╔╝ ██║█████╗██████╔╝█████╗ ██║ ██║██║ ██║█████╗ ███████╗ ██║ ███████╗
██╔═══╝ ██╔══██║██╔══██╗ ██║╚════╝██╔══██╗██╔══╝ ██║▄▄ ██║██║ ██║██╔══╝ ╚════██║ ██║ ╚════██║
██║ ██║ ██║██║ ██║ ██║ ██║ ██║███████╗╚██████╔╝╚██████╔╝███████╗███████║ ██║ ███████║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚══════╝
*/
#region WorkOrderItemPartRequest level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> PartRequestExistsAsync(long id)
{
return await ct.WorkOrderItemPartRequest.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemPartRequest> PartRequestCreateAsync(WorkOrderItemPartRequest newObject)
{
await PartRequestValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemPartRequest.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
//await PartRequestSearchIndexAsync(newObject, true);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await PartRequestHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await PartRequestPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemPartRequest> PartRequestGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemPartRequest.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemPartRequest> PartRequestPutAsync(WorkOrderItemPartRequest putObject)
{
WorkOrderItemPartRequest dbObject = await PartRequestGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
// dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await PartRequestValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await PartRequestExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
// await PartRequestSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await PartRequestHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await PartRequestPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> PartRequestDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await PartRequestGetAsync(id, false);
PartRequestValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemPartRequest.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await PartRequestHandlePotentialNotificationEvent(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;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task PartRequestPopulateVizFields(WorkOrderItemPartRequest o)
{
if (o.PartWarehouseId != 0)
o.PartWarehouseViz = await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync();
Part part = null;
if (o.PartId != 0)
part = await ct.Part.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.PartId);
o.PartViz = part.PartNumber;
o.UpcViz = part.UPC;
PurchaseOrder po = null;
if (o.PurchaseOrderItemId != null)
{
var poid = await ct.PurchaseOrderItem.AsNoTracking().Where(x => x.Id == o.PurchaseOrderItemId).Select(x => x.PurchaseOrderId).FirstOrDefaultAsync();
if (poid != 0)
po = await ct.PurchaseOrder.AsNoTracking().Where(x => x.Id == poid).FirstOrDefaultAsync();
}
if (po != null)
{
o.PurchaseOrderViz = po.Serial.ToString();
o.PurchaseOrderIdViz = po.Id;
o.PurchaseOrderDateViz = po.OrderedDate;
o.PurchaseOrderExpectedDateViz = po.ExpectedReceiveDate;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task PartRequestValidateAsync(WorkOrderItemPartRequest proposedObj, WorkOrderItemPartRequest currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemPartRequest.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);//note: this is passed only to add errors
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void PartRequestValidateCanDelete(WorkOrderItemPartRequest obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemPartRequest))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task PartRequestHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemPartRequest oProposed = (WorkOrderItemPartRequest)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
proposedObj.Tags = WorkorderInfo.Tags;
proposedObj.Name = WorkorderInfo.Serial.ToString();
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item PART REQUEST level
/*
███████╗ ██████╗██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ███████╗██████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗
██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔════╝██╔══██╗ ██║ ██║██╔════╝██╔════╝██╔══██╗██╔════╝
███████╗██║ ███████║█████╗ ██║ ██║██║ ██║██║ █████╗ ██║ ██║█████╗██║ ██║███████╗█████╗ ██████╔╝███████╗
╚════██║██║ ██╔══██║██╔══╝ ██║ ██║██║ ██║██║ ██╔══╝ ██║ ██║╚════╝██║ ██║╚════██║██╔══╝ ██╔══██╗╚════██║
███████║╚██████╗██║ ██║███████╗██████╔╝╚██████╔╝███████╗███████╗██████╔╝ ╚██████╔╝███████║███████╗██║ ██║███████║
╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝
*/
#region WorkOrderItemScheduledUser level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ScheduledUserExistsAsync(long id)
{
return await ct.WorkOrderItemScheduledUser.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemScheduledUser> ScheduledUserCreateAsync(WorkOrderItemScheduledUser newObject)
{
await ScheduledUserValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemScheduledUser.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
//await ScheduledUserSearchIndexAsync(newObject, true);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await ScheduledUserPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemScheduledUser> ScheduledUserGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemScheduledUser.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemScheduledUser> ScheduledUserPutAsync(WorkOrderItemScheduledUser putObject)
{
WorkOrderItemScheduledUser dbObject = await ScheduledUserGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await ScheduledUserValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await ScheduledUserExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
// await ScheduledUserSearchIndexAsync(dbObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await ScheduledUserPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ScheduledUserDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await ScheduledUserGetAsync(id, false);
ScheduledUserValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemScheduledUser.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await ScheduledUserHandlePotentialNotificationEvent(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;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task ScheduledUserPopulateVizFields(WorkOrderItemScheduledUser o)
{
if (o.UserId != null)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
if (o.ServiceRateId != null)
o.ServiceRateViz = await ct.ServiceRate.AsNoTracking().Where(x => x.Id == o.ServiceRateId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ScheduledUserValidateAsync(WorkOrderItemScheduledUser proposedObj, WorkOrderItemScheduledUser currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
// //TEST TEST TEST
// if (proposedObj.EstimatedQuantity == 69)
// {
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, $"EstimatedQuantity", "◈◈ TEST SAVE ERROR ◈◈");
// }
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemScheduledUser.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);//note: this is passed only to add errors
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void ScheduledUserValidateCanDelete(WorkOrderItemScheduledUser obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
// //TEST TEST TEST
// if (obj.EstimatedQuantity == 69)
// {
// AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, $"EstimatedQuantity", "◈◈ TEST DELETE ERROR ◈◈");
// }
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemScheduledUser))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task ScheduledUserHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemScheduledUser oProposed = (WorkOrderItemScheduledUser)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
//## CREATED / UPDATED - ScheduledOnWorkorder event
//Note: scheduled on workorder is immediate so same process regardless if modified or updated
//because modified changes nearly all affect user so decision is just send it no matter what as any difference is enough to send
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
{
//this block is entirely about //ScheduledOnWorkorder event
if (oProposed.UserId != null)
{
//Conditions: userid match and tags
//delivery is immediate so no need to remove old ones of this kind
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorder && z.UserId == oProposed.UserId).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.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ScheduledOnWorkorder,
UserId = sub.UserId,
AyaType = AyaType.WorkOrderItemScheduledUser,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//ScheduledOnWorkorder
}
//---------------------------------------------------------------------------------------------------------------------------------------------
//## CREATED
if (ayaEvent == AyaEvent.Created)
{
//ScheduledOnWorkorderImminent
if (oProposed.UserId != null && oProposed.StartDate != null)
{
//Conditions: userid match and tags + time delayed age value
//delivery is delayed so need to remove old ones of this kind on update
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorderImminent && z.UserId == oProposed.UserId).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.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ScheduledOnWorkorderImminent,
UserId = sub.UserId,
AyaType = AyaType.WorkOrderItemScheduledUser,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
EventDate = (DateTime)oProposed.StartDate,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//ScheduledOnWorkorderImminent
}
//## MODIFIED
if (ayaEvent == AyaEvent.Modified)
{
//ScheduledOnWorkorderImminent
//Always clear any old ones for this object as they are all irrelevant the moment changed:
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.ScheduledOnWorkorderImminent);
if (oProposed.UserId != null && oProposed.StartDate != null)
{
//Conditions: userid match and tags + time delayed age value
//delivery is delayed so need to remove old ones of this kind on update
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.ScheduledOnWorkorderImminent && z.UserId == oProposed.UserId).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.ObjectHasAllSubscriptionTags(WorkorderInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.ScheduledOnWorkorderImminent,
UserId = sub.UserId,
AyaType = AyaType.WorkOrderItemScheduledUser,
ObjectId = oProposed.Id,
NotifySubscriptionId = sub.Id,
EventDate = (DateTime)oProposed.StartDate,
Name = $"{WorkorderInfo.Serial.ToString()}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//ScheduledOnWorkorderImminent
}
}//end of process notifications
#endregion work order item SCHEDULED USER level
/*
████████╗ █████╗ ███████╗██╗ ██╗
╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██║ ███████║███████╗█████╔╝
██║ ██╔══██║╚════██║██╔═██╗
██║ ██║ ██║███████║██║ ██╗
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
*/
#region WorkOrderItemTask level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> TaskExistsAsync(long id)
{
return await ct.WorkOrderItemTask.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemTask> TaskCreateAsync(WorkOrderItemTask newObject)
{
await TaskValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemTask.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await TaskSearchIndexAsync(newObject, true);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await TaskHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await TaskPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemTask> TaskGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemTask.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemTask> TaskPutAsync(WorkOrderItemTask putObject)
{
WorkOrderItemTask dbObject = await TaskGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
//dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await TaskValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await TaskExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await TaskSearchIndexAsync(dbObject, false);
// await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await TaskHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await TaskPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> TaskDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await TaskGetAsync(id, false);
TaskValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemTask.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await TaskHandlePotentialNotificationEvent(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;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task TaskSearchIndexAsync(WorkOrderItemTask obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.Task);//some are manually entered so this is worthwhile for that at least, also I guess predefined tasks that are more rare
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> TaskGetSearchResultSummary(long id)
{
var obj = await TaskGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Task);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task TaskPopulateVizFields(WorkOrderItemTask o, List<NameIdItem> taskCompletionTypeEnumList = null)
{
if (o.CompletedByUserId != null)
o.CompletedByUserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.CompletedByUserId).Select(x => x.Name).FirstOrDefaultAsync();
if (taskCompletionTypeEnumList == null)
taskCompletionTypeEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList(
StringUtil.TrimTypeName(typeof(WorkorderItemTaskCompletionType).ToString()),
UserTranslationId,
CurrentUserRoles);
o.StatusViz = taskCompletionTypeEnumList.Where(x => x.Id == (long)o.Status).Select(x => x.Name).First();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task TaskValidateAsync(WorkOrderItemTask proposedObj, WorkOrderItemTask currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (string.IsNullOrWhiteSpace(proposedObj.Task))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Task");
//TEST TEST TEST ERROR
if (proposedObj.Sequence == 999)
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Sequence");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemTask.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);//note: this is passed only to add errors
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void TaskValidateCanDelete(WorkOrderItemTask obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemTask))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task TaskHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemTask oProposed = (WorkOrderItemTask)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item TASK level
/*
████████╗██████╗ █████╗ ██╗ ██╗███████╗██╗
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║██╔════╝██║
██║ ██████╔╝███████║██║ ██║█████╗ ██║
██║ ██╔══██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██║
██║ ██║ ██║██║ ██║ ╚████╔╝ ███████╗███████╗
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝
*/
#region WorkOrderItemTravel level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> TravelExistsAsync(long id)
{
return await ct.WorkOrderItemTravel.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemTravel> TravelCreateAsync(WorkOrderItemTravel newObject)
{
await TravelValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
//await TravelBizActionsAsync(AyaEvent.Created, newObject, null, null);
//newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
//newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemTravel.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await TravelSearchIndexAsync(newObject, true);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await TravelHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await TravelPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemTravel> TravelGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemTravel.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemTravel> TravelPutAsync(WorkOrderItemTravel putObject)
{
WorkOrderItemTravel dbObject = await TravelGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
//dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
// dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await TravelValidateAsync(putObject, dbObject);
if (HasErrors) return null;
// await TravelBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await TravelExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await TravelSearchIndexAsync(putObject, false);
//await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, dbObject.Tags, SnapshotOfOriginalDBObj.Tags);
await TravelHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await TravelPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> TravelDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await TravelGetAsync(id, false);
TravelValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemTravel.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
// await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(BizType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await TravelHandlePotentialNotificationEvent(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;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task TravelSearchIndexAsync(WorkOrderItemTravel obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.TravelDetails);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> TravelGetSearchResultSummary(long id)
{
var obj = await TravelGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.TravelDetails);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task TravelPopulateVizFields(WorkOrderItemTravel o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.UserId != null)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
}
TravelRate Rate = null;
if (o.TravelRateId != null)
{
Rate = await ct.TravelRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.TravelRateId);
o.TravelRateViz = Rate.Name;
}
TaxCode Tax = null;
if (o.TaxCodeSaleId != null)
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
if (Tax != null)
o.TaxCodeSaleViz = Tax.Name;
o.PriceViz = 0;
if (Rate != null)
{
o.CostViz = Rate.Cost;
o.ListPriceViz = Rate.Charge;
o.UnitOfMeasureViz = Rate.Unit;
o.PriceViz = Rate.Charge;//default price used if not manual or contract override
}
//manual price overrides anything
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
else
{
//not manual so could potentially have a contract adjustment
var c = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, o.WorkOrderItemId);
if (c != null)
{
decimal pct = 0;
ContractOverrideType cot = ContractOverrideType.PriceDiscount;
bool TaggedAdjustmentInEffect = false;
//POTENTIAL CONTRACT ADJUSTMENTS
//First check if there is a matching tagged Travel rate contract discount, that takes precedence
if (c.ContractTravelRateOverrideItems.Count > 0)
{
//Iterate all contract tagged items in order of ones with the most tags first
foreach (var csr in c.ContractTravelRateOverrideItems.OrderByDescending(z => z.Tags.Count))
if (csr.Tags.All(z => Rate.Tags.Any(x => x == z)))
{
if (csr.OverridePct != 0)
{
pct = csr.OverridePct / 100;
cot = csr.OverrideType;
TaggedAdjustmentInEffect = true;
}
}
}
//Generic discount?
if (!TaggedAdjustmentInEffect && c.TravelRatesOverridePct != 0)
{
pct = c.TravelRatesOverridePct / 100;
cot = c.TravelRatesOverrideType;
}
//apply if discount found
if (pct != 0)
{
if (cot == ContractOverrideType.CostMarkup)
o.PriceViz = o.CostViz + (o.CostViz * pct);
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = o.ListPriceViz - (o.ListPriceViz * pct);
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz * o.TravelRateQuantity;
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = o.NetViz * (Tax.TaxAPct / 100);
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = (o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100);
}
else
{
o.TaxBViz = o.NetViz * (Tax.TaxBPct / 100);
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
}
// ////////////////////////////////////////////////////////////////////////////////////////////////
// //BIZ ACTIONS
// //
// //
// private async Task TravelBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemTravel newObj, WorkOrderItemTravel oldObj, IDbContextTransaction transaction)
// {
// //automatic actions on record change, called AFTER validation
// //currently no processing required except for created or modified at this time
// if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
// return;
// //SET TAXES AND PRICING
// //by default apply all automatic actions with further restrictions possible below
// bool ApplyTax = true;
// bool ApplyPricingUpdate = true;
// //if modifed, see what has changed and should be re-applied
// if (ayaEvent == AyaEvent.Modified)
// {
// //If it wasn't a service rate change there is no need to set pricing
// if (newObj.TravelRateId == oldObj.TravelRateId)
// {
// ApplyPricingUpdate = false;
// }
// //If taxes haven't change then no need to update taxes
// if (newObj.TaxCodeSaleId == oldObj.TaxCodeSaleId)
// ApplyTax = false;
// }
// //Tax code
// if (ApplyTax)
// {
// //Default in case nothing to apply
// newObj.TaxAPct = 0;
// newObj.TaxBPct = 0;
// newObj.TaxOnTax = false;
// if (newObj.TaxCodeSaleId != null)
// {
// var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TaxCodeSaleId);
// if (t != null)
// {
// newObj.TaxAPct = t.TaxAPct;
// newObj.TaxBPct = t.TaxBPct;
// newObj.TaxOnTax = t.TaxOnTax;
// }
// }
// }
// //Pricing
// if (ApplyPricingUpdate)
// {
// //default in case nothing to apply
// newObj.Cost = 0;
// newObj.ListPrice = 0;
// newObj.Price = 0;
// //in v7 it was ok to have no service rate selected
// //not sure why but carried forward to v8 so..
// if (newObj.TravelRateId != null)
// {
// var s = await ct.TravelRate.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TravelRateId);
// if (s != null)
// {
// newObj.Cost = s.Cost;
// newObj.ListPrice = s.Charge;
// var Contract = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, newObj.WorkOrderItemId);
// TravelSetListPrice(newObj, Contract);
// }
// }
// }
// }
// ////////////////////////////////////////////////////////////////////////////////////////////////
// // SET PER UNIT LIST PRICE
// //
// //(called by woitemtravel save and also by header save on change of contract)
// private static void TravelSetListPrice(WorkOrderItemTravel o, Contract c)
// {
// if (c == null || c.ServiceRatesOverridePct == 0)
// {
// o.Price = o.ListPrice;//default with no contract
// return;
// }
// if (c.ServiceRatesOverrideType == ContractOverrideType.CostMarkup)
// o.Price = o.Cost + (o.Cost * c.ServiceRatesOverridePct);
// else if (c.ServiceRatesOverrideType == ContractOverrideType.PriceDiscount)
// o.Price = o.ListPrice - (o.ListPrice * c.ServiceRatesOverridePct);
// }
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task TravelValidateAsync(WorkOrderItemTravel proposedObj, WorkOrderItemTravel currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemTravel.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);//note: this is passed only to add errors
//validate custom fields
//CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void TravelValidateCanDelete(WorkOrderItemTravel obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemTravel))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task TravelHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemTravel oProposed = (WorkOrderItemTravel)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item TRAVEL level
/*
██╗ ██╗███╗ ██╗██╗████████╗
██║ ██║████╗ ██║██║╚══██╔══╝
██║ ██║██╔██╗ ██║██║ ██║
██║ ██║██║╚██╗██║██║ ██║
╚██████╔╝██║ ╚████║██║ ██║
╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
*/
#region WorkOrderItemUnit level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> UnitExistsAsync(long id)
{
return await ct.WorkOrderItemUnit.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<WorkOrderItemUnit> UnitCreateAsync(WorkOrderItemUnit newObject)
{
await UnitValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
//TODO: In biz actions set contract if this unit has a contract, note that we are only here if there is no pre-existing unit with a contract on this workorder via validation above
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.WorkOrderItemUnit.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await UnitSearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await UnitHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
await UnitPopulateVizFields(newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<WorkOrderItemUnit> UnitGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.WorkOrderItemUnit.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<WorkOrderItemUnit> UnitPutAsync(WorkOrderItemUnit putObject)
{
WorkOrderItemUnit dbObject = await UnitGetAsync(putObject.Id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return null;
}
if (dbObject.Concurrency != putObject.Concurrency)
{
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
dbObject.Tags = TagBiz.NormalizeTags(dbObject.Tags);
dbObject.CustomFields = JsonUtil.CompactJson(dbObject.CustomFields);
await UnitValidateAsync(putObject, dbObject);
if (HasErrors) return null;
//TODO: In biz actions set contract if this unit has a contract, note that we are only here if there is no pre-existing unit with a contract on this workorder via validation above
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await UnitExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await UnitSearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await UnitHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
await UnitPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> UnitDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await UnitGetAsync(id, false);
UnitValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemUnit.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "woitem:" + dbObject.WorkOrderItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
//await TagBiz.ProcessDeleteTagsInRepositoryAsync(ct, dbObject.Tags);
//await FileUtil.DeleteAttachmentsForObjectAsync(dbObject.AyaType, dbObject.Id, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await UnitHandlePotentialNotificationEvent(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;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task UnitSearchIndexAsync(WorkOrderItemUnit obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
if (isNew)
await Search.ProcessNewObjectKeywordsAsync(SearchParams);
else
await Search.ProcessUpdatedObjectKeywordsAsync(SearchParams);
}
public async Task<Search.SearchIndexProcessObjectParameters> UnitGetSearchResultSummary(long id)
{
var obj = await UnitGetAsync(id, false);
var SearchParams = new Search.SearchIndexProcessObjectParameters();
if (obj != null)
SearchParams.AddText(obj.Notes).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task UnitPopulateVizFields(WorkOrderItemUnit o)
{
o.UnitViz = await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task UnitValidateAsync(WorkOrderItemUnit proposedObj, WorkOrderItemUnit currentObj)
{
//skip validation if seeding
// if (ServerBootConfig.SEEDING) return;
//TODO: ADD VALIDATIONS:
// - A work order *MUST* have only one Unit with a Contract, if there is already a unit with a contract on this workorder then a new one cannot be added and it will reject with a validation error
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.WorkOrderItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentWorkOrderStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("WorkOrderErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (proposedObj.UnitId < 1 || !await ct.Unit.AnyAsync(x => x.Id == proposedObj.UnitId))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.WorkOrderItemUnit.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);//note: this is passed only to add errors
//validate custom fields
CustomFieldsValidator.Validate(this, FormCustomization, proposedObj.CustomFields);
}
}
private void UnitValidateCanDelete(WorkOrderItemUnit obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.WorkOrderItemUnit))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task UnitHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
WorkOrderItemUnit oProposed = (WorkOrderItemUnit)proposedObj;
var woId = await GetWorkOrderIdFromRelativeAsync(AyaType.WorkOrderItem, oProposed.WorkOrderItemId, ct);
var WorkorderInfo = await ct.WorkOrder.AsNoTracking().Where(x => x.Id == woId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item LABOR level
#region Utility
public async Task<ICoreBizObjectModel> GetWorkOrderGraphItem(AyaType ayaType, long id)
{
switch (ayaType)
{
case AyaType.WorkOrder:
return await WorkOrderGetAsync(id, false) as ICoreBizObjectModel;
case AyaType.WorkOrderItem:
return await ItemGetAsync(id, false);
case AyaType.WorkOrderItemExpense:
return await ExpenseGetAsync(id, false);
case AyaType.WorkOrderItemLabor:
return await LaborGetAsync(id, false);
case AyaType.WorkOrderItemLoan:
return await LoanGetAsync(id, false);
case AyaType.WorkOrderItemPart:
return await PartGetAsync(id, false);
case AyaType.WorkOrderItemPartRequest:
return await PartRequestGetAsync(id, false);
case AyaType.WorkOrderItemScheduledUser:
return await ScheduledUserGetAsync(id, false);
case AyaType.WorkOrderItemTask:
return await TaskGetAsync(id, false);
case AyaType.WorkOrderItemTravel:
return await TravelGetAsync(id, false);
case AyaType.WorkOrderItemUnit:
return await UnitGetAsync(id, false);
case AyaType.WorkOrderItemOutsideService:
return await OutsideServiceGetAsync(id, false);
default:
throw new System.ArgumentOutOfRangeException($"WorkOrder::GetWorkOrderGraphItem -> Invalid ayaType{ayaType}");
}
}
public async Task<ICoreBizObjectModel> PutWorkOrderGraphItem(AyaType ayaType, ICoreBizObjectModel o)
{
ClearErrors();
switch (ayaType)
{
case AyaType.WorkOrder:
if (o is WorkOrder)
{
WorkOrder dto = new WorkOrder();
CopyObject.Copy(o, dto);
return await WorkOrderPutAsync((WorkOrder)dto);
}
return await WorkOrderPutAsync((WorkOrder)o) as ICoreBizObjectModel;
case AyaType.WorkOrderItem:
if (o is WorkOrderItem)
{
WorkOrderItem dto = new WorkOrderItem();
CopyObject.Copy(o, dto);
return await ItemPutAsync((WorkOrderItem)dto);
}
return await ItemPutAsync((WorkOrderItem)o);
case AyaType.WorkOrderItemExpense:
return await ExpensePutAsync((WorkOrderItemExpense)o);
case AyaType.WorkOrderItemLabor:
return await LaborPutAsync((WorkOrderItemLabor)o);
case AyaType.WorkOrderItemLoan:
return await LoanPutAsync((WorkOrderItemLoan)o);
case AyaType.WorkOrderItemPart:
return await PartPutAsync((WorkOrderItemPart)o);
case AyaType.WorkOrderItemPartRequest:
return await PartRequestPutAsync((WorkOrderItemPartRequest)o);
case AyaType.WorkOrderItemScheduledUser:
return await ScheduledUserPutAsync((WorkOrderItemScheduledUser)o);
case AyaType.WorkOrderItemTask:
return await TaskPutAsync((WorkOrderItemTask)o);
case AyaType.WorkOrderItemTravel:
return await TravelPutAsync((WorkOrderItemTravel)o);
case AyaType.WorkOrderItemUnit:
return await UnitPutAsync((WorkOrderItemUnit)o);
case AyaType.WorkOrderItemOutsideService:
return await OutsideServicePutAsync((WorkOrderItemOutsideService)o);
default:
throw new System.ArgumentOutOfRangeException($"WorkOrder::PutWorkOrderGraphItem -> Invalid ayaType{ayaType}");
}
}
public async Task<bool> DeleteWorkOrderGraphItem(AyaType ayaType, long id)
{
switch (ayaType)
{
case AyaType.WorkOrder:
return await WorkOrderDeleteAsync(id);
case AyaType.WorkOrderItem:
return await ItemDeleteAsync(id);
case AyaType.WorkOrderItemExpense:
return await ExpenseDeleteAsync(id);
case AyaType.WorkOrderItemLabor:
return await LaborDeleteAsync(id);
case AyaType.WorkOrderItemLoan:
return await LoanDeleteAsync(id);
case AyaType.WorkOrderItemPart:
return await PartDeleteAsync(id);
case AyaType.WorkOrderItemPartRequest:
return await PartRequestDeleteAsync(id);
case AyaType.WorkOrderItemScheduledUser:
return await ScheduledUserDeleteAsync(id);
case AyaType.WorkOrderItemTask:
return await TaskDeleteAsync(id);
case AyaType.WorkOrderItemTravel:
return await TravelDeleteAsync(id);
case AyaType.WorkOrderItemUnit:
return await UnitDeleteAsync(id);
case AyaType.WorkOrderItemOutsideService:
return await OutsideServiceDeleteAsync(id);
default:
throw new System.ArgumentOutOfRangeException($"WorkOrder::GetWorkOrderGraphItem -> Invalid ayaType{ayaType}");
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET CONTRACT FOR WORKORDER FROM RELATIVE
//
//cache the contract to save repeatedly fetching it for this operation
internal Contract mContractInEffect = null;
internal bool mFetchedContractAlready = false;//null contract isn't enough to know it was fetched as it could just not have a contract so this is required
internal async Task<Contract> GetCurrentWorkOrderContractFromRelatedAsync(AyaType ayaType, long id)
{
if (mFetchedContractAlready == false)
{
long WoId = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct);
var WoContractId = await ct.WorkOrder.AsNoTracking().Where(z => z.Id == WoId).Select(z => z.ContractId).FirstOrDefaultAsync();
await GetCurrentContractFromContractIdAsync(WoContractId);
}
return mContractInEffect;
}
internal async Task<Contract> GetCurrentContractFromContractIdAsync(long? id)
{
if (id == null) return null;
if (mFetchedContractAlready == false)
{
mContractInEffect = await ct.Contract.AsSplitQuery().AsNoTracking()
.Include(c => c.ServiceRateItems)
.Include(c => c.TravelRateItems)
.Include(c => c.ContractPartOverrideItems)
.Include(c => c.ContractTravelRateOverrideItems)
.Include(c => c.ContractServiceRateOverrideItems)
.FirstOrDefaultAsync(z => z.Id == id);
}
return mContractInEffect;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET CURRENT STATUS FOR WORKORDER FROM RELATIVE
//
internal async Task<WorkOrderStatus> GetCurrentWorkOrderStatusFromRelatedAsync(AyaType ayaType, long id)
{
//instantiated method to save adding the context
return await GetCurrentWorkOrderStatusFromRelatedAsync(ayaType, id, ct);
}
internal static async Task<WorkOrderStatus> GetCurrentWorkOrderStatusFromRelatedAsync(AyaType ayaType, long id, AyContext ct)
{
//static method
long WoId = await GetWorkOrderIdFromRelativeAsync(ayaType, id, ct);
var stat = await ct.WorkOrderState.AsNoTracking()
.Where(z => z.WorkOrderId == WoId)
.OrderByDescending(z => z.Created)
.Take(1)
.FirstOrDefaultAsync();
//no state set yet?
if (stat == null)
{ //default
return new WorkOrderStatus() { Id = -1, Locked = false, Completed = false };
}
return await ct.WorkOrderStatus.AsNoTracking().Where(z => z.Id == stat.WorkOrderStatusId).FirstAsync();//this should never not be null
}
#endregion utility
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons