Files
raven/server/AyaNova/biz/WorkOrderBiz.cs
2021-05-21 14:17:31 +00:00

4824 lines
226 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)
{
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);
//Was this a full workorder posted all at once?
//(seeder or api user, not something AyaNova front end would do)
if (newObject.Items.Count > 0)//our front end will post the header alone on new so this indicates a fully populated wo was saved
{
await GetCurrentContractFromContractIdAsync(newObject.ContractId);
await ProcessChangeOfContractAsync(newObject.Id);
}
await WorkOrderPopulateVizFields(newObject, 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.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.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)
.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;
ret.IsDirty = false;
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)
{
//todo: bizactions, contract change update pricing entire workorder and return it entirely?
//no client must fetch it, just return concurrency as per normal here
//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);
bool contractChanged = false;
long? newContractId = null;
if (putObject.ContractId != dbObject.ContractId)//manual change of contract
{
contractChanged = true;
newContractId = putObject.ContractId;
}
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);
if (contractChanged)
{
await GetCurrentContractFromContractIdAsync(newContractId);
await ProcessChangeOfContractAsync(putObject.Id);
}
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);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CONTRACT CHANGE HANDLER
//
//
private async Task<WorkOrder> ProcessChangeOfContractAsync(long woId)
{
//contract has changed, update entire graph pricing and potentially response time stuff as well here now
//iterate graph calling *SetListPrice on each item
var wo = await ct.WorkOrder
.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)
.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 == woId);
//If Contract has response time then set CompleteByDate
if (mContractInEffect != null && mContractInEffect.ResponseTime != TimeSpan.Zero)
{
wo.CompleteByDate = DateTime.UtcNow.Add(mContractInEffect.ResponseTime);
}
// //update pricing
// foreach (WorkOrderItem wi in wo.Items)
// {
// // foreach (WorkOrderItemLabor o in wi.Labors)
// // await LaborSetPrice(o, mContractInEffect);
// foreach (WorkOrderItemTravel o in wi.Travels)
// TravelSetListPrice(o, mContractInEffect);
// foreach (WorkOrderItemPart o in wi.Parts)
// PartSetListPrice(o, mContractInEffect);
// }
await ct.SaveChangesAsync();
return wo;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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
WorkorderFinished = 30, //*travel work order is set to any status that is flagged as a "Finished" type of status. Customer & User
//This one could be accomplished with WorkorderStatusAge, just pick a finished status and set a time frame and wala!
WorkorderFinishedFollowUp = 32, //* travel workorder closed status follow up again after this many TIMESPAN
todo: CHANGE WorkorderFinishStatusOverdue = 15,//* Workorder object not set to a "Finished" 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 Finished 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;
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 customerAlert = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => x.PopUpNotes).FirstOrDefaultAsync();
if (!string.IsNullOrWhiteSpace(customerAlert))
{
o.AlertViz = $"{await Translate("Customer")}\n{customerAlert}\n\n";
}
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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
//todo: contract response time notification
}//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);
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)
{
await StateHandlePotentialNotificationEvent(AyaEvent.Deleted, wostate);
ct.WorkOrderState.Remove(wostate);
await ct.SaveChangesAsync();
}
}
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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderState o = (WorkOrderState)proposedObj;
//## 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 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.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 UnitIds)
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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItem o = (WorkOrderItem)proposedObj;
//## 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)
{
if (o.UserId != null)
o.UserViz = await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync();
// if (o.ChargeTaxCodeId != null)
// o.ChargeTaxCodeViz = await ct.TaxCode.AsNoTracking().Where(x => x.Id == o.ChargeTaxCodeId).Select(x => x.Name).FirstOrDefaultAsync();
//Calculate totals and taxes
o.TaxAViz = 0;
o.TaxBViz = 0;
if (o.TaxAPct != 0)
{
o.TaxAViz = o.ChargeAmount * (o.TaxAPct / 100);
}
if (o.TaxBPct != 0)
{
if (o.TaxOnTax)
{
o.TaxBViz = (o.ChargeAmount + o.TaxAViz) * (o.TaxBPct / 100);
}
else
{
o.TaxBViz = o.ChargeAmount * (o.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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
//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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemExpense o = (WorkOrderItemExpense)proposedObj;
//## 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 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();
newObject.IsDirty = false;
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();
putObject.IsDirty = false;
}
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)
{
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);
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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemLabor o = (WorkOrderItemLabor)proposedObj;
//## 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 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
{
//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;
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)
{
await Task.CompletedTask;
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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.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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemLoan o = (WorkOrderItemLoan)proposedObj;
//## 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 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)
{
await Task.CompletedTask;
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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.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)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<WorkOrderBiz>();
if (ServerBootConfig.SEEDING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemOutsideService o = (WorkOrderItemOutsideService)proposedObj;
//## 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 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)
{
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 EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await PartSearchIndexAsync(newObject, true);
//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)
{
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();
}
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 PartPopulateVizFields(putObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> PartDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
try
{
var dbObject = await PartGetAsync(id, false);
PartValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.WorkOrderItemPart.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 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)
{
await Task.CompletedTask;
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task PartBizActionsAsync(AyaEvent ayaEvent, WorkOrderItemPart newObj, WorkOrderItemPart 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 complete part change there is no need to set pricing
if (newObj.PartId == oldObj.PartId)
{
ApplyPricingUpdate = false;
}
//If taxes haven't change then no need to update taxes
if (newObj.TaxPartSaleId == oldObj.TaxPartSaleId)
ApplyTax = false;
}
//Tax code
if (ApplyTax)
{
//Default in case nothing to apply
newObj.TaxAPct = 0;
newObj.TaxBPct = 0;
newObj.TaxOnTax = false;
if (newObj.TaxPartSaleId != null)
{
var t = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.TaxPartSaleId);
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;
var s = await ct.Part.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.PartId);
if (s != null)
{
newObj.Cost = s.Cost;
newObj.ListPrice = s.Retail;
var Contract = await GetCurrentWorkOrderContractFromRelatedAsync(AyaType.WorkOrderItem, newObj.WorkOrderItemId);
PartSetListPrice(newObj, Contract);
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// SET PER UNIT LIST PRICE
//
//(called by woitempart save and also by header save on change of contract)
private static void PartSetListPrice(WorkOrderItemPart 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 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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemPart o = (WorkOrderItemPart)proposedObj;
//## 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 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 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;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task PartRequestPopulateVizFields(WorkOrderItemPartRequest o)
{
await Task.CompletedTask;
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemPartRequest o = (WorkOrderItemPartRequest)proposedObj;
//## 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 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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemScheduledUser o = (WorkOrderItemScheduledUser)proposedObj;
//## 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 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)
{
await Task.CompletedTask;
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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.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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemTask o = (WorkOrderItemTask)proposedObj;
//## 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 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)
{
await Task.CompletedTask;
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).FirstOrDefaultAsync();
}
////////////////////////////////////////////////////////////////////////////////////////////////
//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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemTravel o = (WorkOrderItemTravel)proposedObj;
//## 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 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)
{
await Task.CompletedTask;
// if (o.WorkOrderOverseerId != null)
// o.WorkOrderOverseerViz = await ct.User.AsNoTracking().Where(x => x.Id == o.WorkOrderOverseerId).Select(x => x.Name).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");
else if (!await ItemExistsAsync(proposedObj.WorkOrderItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "WorkOrderItemId");
}
//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.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;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
WorkOrderItemUnit o = (WorkOrderItemUnit)proposedObj;
//## 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 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.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