Files
raven/server/AyaNova/biz/QuoteBiz.cs
2022-03-08 01:23:16 +00:00

5230 lines
241 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 QuoteBiz : BizObject, IJobObject, ISearchAbleObject, IReportAbleObject, IExportAbleObject
{
internal QuoteBiz(AyContext dbcontext, long currentUserId, long userTranslationId, AuthorizationRoles UserRoles, UserType currentUserType)
{
ct = dbcontext;
UserId = currentUserId;
UserTranslationId = userTranslationId;
CurrentUserRoles = UserRoles;
BizType = AyaType.Quote;
CurrentUserType = currentUserType;
//Sub-role rights flags
UserIsTechRestricted = CurrentUserRoles.HasFlag(AuthorizationRoles.TechRestricted);
UserIsSubContractorFull = CurrentUserType == UserType.ServiceContractor && CurrentUserRoles.HasFlag(AuthorizationRoles.SubContractor);
UserIsSubContractorRestricted = CurrentUserType == UserType.ServiceContractor && CurrentUserRoles.HasFlag(AuthorizationRoles.SubContractorRestricted);
UserIsRestrictedType = UserIsTechRestricted || UserIsSubContractorFull || UserIsSubContractorRestricted;
UserCanViewPartCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.InventoryRestricted)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.Inventory)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
UserCanViewLaborOrTravelRateCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.Service)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.ServiceRestricted)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
UserCanViewLoanerCosts = CurrentUserRoles.HasFlag(AuthorizationRoles.Service)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.ServiceRestricted)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.BizAdmin)
|| CurrentUserRoles.HasFlag(AuthorizationRoles.Accounting);
}
internal static QuoteBiz GetBiz(AyContext ct, Microsoft.AspNetCore.Http.HttpContext httpContext = null)
{
if (httpContext != null)
return new QuoteBiz(ct,
UserIdFromContext.Id(httpContext.Items),
UserTranslationIdFromContext.Id(httpContext.Items),
UserRolesFromContext.Roles(httpContext.Items),
UserTypeFromContext.Type(httpContext.Items));
else
return new QuoteBiz(ct,
1,
ServerBootConfig.AYANOVA_DEFAULT_TRANSLATION_ID,
AuthorizationRoles.BizAdmin,
UserType.NotService);//picked not service arbitrarily, probably a non-factor
}
private VizCache vc = new VizCache();
private ObjectCache oc = new ObjectCache();
/*
██████╗ ██╗ ██╗ ██████╗ ████████╗███████╗
██╔═══██╗██║ ██║██╔═══██╗╚══██╔══╝██╔════╝
██║ ██║██║ ██║██║ ██║ ██║ █████╗
██║▄▄ ██║██║ ██║██║ ██║ ██║ ██╔══╝
╚██████╔╝╚██████╔╝╚██████╔╝ ██║ ███████╗
╚══▀▀═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝
*/
#region Quote level
////////////////////////////////////////////////////////////////////////////////////////////////
// SUBRIGHTS / RESTRICTIONS FOR WORK ORDER
//
//Note: these restrictions and rights are in addition to the basic fundamental role access rights (layer 1)
//and are considered after role rights have already been consulted first (usually at the controller level)
internal UserType CurrentUserType { get; set; }
internal bool UserIsRestrictedType { get; set; }
internal bool UserIsTechRestricted { get; set; }
internal bool UserIsSubContractorFull { get; set; }
internal bool UserIsSubContractorRestricted { get; set; }
internal bool UserCanViewPartCosts { get; set; }
internal bool UserCanViewLaborOrTravelRateCosts { get; set; }
internal bool UserCanViewLoanerCosts { get; set; }
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> QuoteExistsAsync(long id)
{
return await ct.Quote.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<Quote> QuoteCreateAsync(Quote newObject, bool populateViz = true)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
await QuoteValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await QuoteBizActionsAsync(AyaEvent.Created, newObject, null, null);
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.Quote.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, BizType, AyaEvent.Created), ct);
await QuoteSearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
//# NOTE: only internal code can post an entire quote graph, no external user can as controller will reject right up front
//however, internally seeder will post entire workorders
if (newObject.Items.Count > 0)
{
await GetCurrentContractFromContractIdAsync(newObject.ContractId);
//GRANDCHILD BIZ ACTIONS
foreach (QuoteItem wi in newObject.Items)
{
foreach (QuoteItemPart wip in wi.Parts)
await PartBizActionsAsync(AyaEvent.Created, wip, null, null);
foreach (QuoteItemLoan wil in wi.Loans)
await LoanBizActionsAsync(AyaEvent.Created, wil, null, null);
}
await ct.SaveChangesAsync();
//NOTE: not running individual notification here for children, seeder won't require it and that's all that posts an entire wo currently
}
await transaction.CommitAsync();
if (populateViz)
await QuotePopulateVizFields(newObject, true, false);
if (newObject.GenCopyAttachmentsFrom != null && !newObject.GenCopyAttachmentsFrom.IsEmpty)
{
//copy attachment from existing object
await AttachmentBiz.DuplicateAttachments(newObject.GenCopyAttachmentsFrom, new AyaTypeId(AyaType.Quote, newObject.Id), ct);
newObject.GenCopyAttachmentsFrom = null;//so it doesn't get returned
}
await QuoteHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
}
//quote needs to be fetched internally from several places for rule checking etc
//this just gets it raw and lets others process
private async Task<Quote> QuoteGetFullAsync(long id)
{
//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
return await ct.Quote.AsSplitQuery().AsNoTracking()
.Include(s => s.States)
.Include(w => w.Items.OrderBy(item => item.Sequence))
.ThenInclude(wi => wi.Expenses)
.Include(w => w.Items)
.ThenInclude(wi => wi.Labors)
.Include(w => w.Items)
.ThenInclude(wi => wi.Loans)
.Include(w => w.Items)
.ThenInclude(wi => wi.Parts)
.Include(w => w.Items)
.ThenInclude(wi => wi.ScheduledUsers)
.Include(w => w.Items)
.ThenInclude(wi => wi.Tasks.OrderBy(t => t.Sequence))
.Include(w => w.Items)
.ThenInclude(wi => wi.Travels)
.Include(w => w.Items)
.ThenInclude(wi => wi.Units)
.Include(w => w.Items)
.ThenInclude(wi => wi.OutsideServices)
.SingleOrDefaultAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<Quote> QuoteGetAsync(long id, bool populateDisplayFields, bool logTheGetEvent = true, bool populateForReporting = false)
{
var ret = await QuoteGetFullAsync(id);
if (ret != null)
{
var stat = await GetCurrentQuoteStatusFromRelatedAsync(BizType, ret.Id);
ret.IsLockedAtServer = stat.Locked;
var userIsTechRestricted = UserIsTechRestricted;
var userIsSubContractorFull = UserIsSubContractorFull;
var userIsSubContractorRestricted = UserIsSubContractorRestricted;
var userIsRestricted = (userIsTechRestricted || userIsSubContractorFull || userIsSubContractorRestricted);
if (userIsRestricted)
{
//Restricted users can only work with quote items they are scheduled on
List<QuoteItem> removeItems = new List<QuoteItem>();
//gather list of items to remove by checking if they are scheduled on them or not
foreach (QuoteItem wi in ret.Items)
{
var userIsSelfScheduledOnThisItem = false;
foreach (QuoteItemScheduledUser su in wi.ScheduledUsers)
{
if (su.UserId == UserId)
{
userIsSelfScheduledOnThisItem = true;
break;
}
}
if (!userIsSelfScheduledOnThisItem) removeItems.Add(wi);
}
foreach (var removeitem in removeItems)
{
ret.Items.Remove(removeitem);
ret.IsCompleteRecord = false;
}
//Restricted users may have further restrictions
foreach (QuoteItem wi in ret.Items)
{
//all restricted types
wi.ScheduledUsers.RemoveAll(x => x.UserId != UserId);
wi.Labors.RemoveAll(x => x.UserId != UserId);
wi.Travels.RemoveAll(x => x.UserId != UserId);
if (userIsTechRestricted)
{
wi.Expenses.RemoveAll(x => x.UserId != UserId);
}
if (userIsSubContractorFull)
{
wi.Expenses.RemoveAll(x => true);
wi.OutsideServices.RemoveAll(x => true);
}
if (userIsSubContractorRestricted)
{
wi.Units.RemoveAll(x => true);
wi.Parts.RemoveAll(x => true);
wi.Expenses.RemoveAll(x => true);
wi.Loans.RemoveAll(x => true);
wi.OutsideServices.RemoveAll(x => true);
}
//tasks are allowed to be viewed and update the task completion types
}
}
if (populateDisplayFields)
await QuotePopulateVizFields(ret, false, populateForReporting);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, BizType, AyaEvent.Retrieved), ct);
}
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<Quote> QuotePutAsync(Quote putObject)
{
//## PUT HEADER ONLY, NO ALLOWANCE FOR PUT OF ENTIRE WORKORDER
//Note: this is intentionally not using the getasync because
//doing so would invoke the children which would then get deleted on save since putobject has no children
Quote dbObject = await ct.Quote.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 QuoteValidateAsync(putObject, dbObject);
if (HasErrors)
return null;
await QuoteBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
long? newContractId = null;
if (putObject.ContractId != dbObject.ContractId)//manual change of contract
{
newContractId = putObject.ContractId;
await GetCurrentContractFromContractIdAsync(newContractId);
}
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await QuoteExistsAsync(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 QuoteSearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await QuotePopulateVizFields(putObject, true, false);
await QuoteHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> QuoteDeleteAsync(long id)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
Quote dbObject = await ct.Quote.AsNoTracking().Where(z => z.Id == id).FirstOrDefaultAsync();// QuoteGetAsync(id, false);
if (dbObject == null)
{
AddError(ApiErrorCode.NOT_FOUND);
return false;
}
await QuoteValidateCanDelete(dbObject);
if (HasErrors)
return false;
{
var IDList = await ct.Review.AsNoTracking().Where(x => x.AType == AyaType.Quote && x.ObjectId == id).Select(x => x.Id).ToListAsync();
if (IDList.Count() > 0)
{
ReviewBiz b = new ReviewBiz(ct, UserId, UserTranslationId, CurrentUserRoles);
foreach (long ItemId in IDList)
if (!await b.DeleteAsync(ItemId, transaction))
{
AddError(ApiErrorCode.CHILD_OBJECT_ERROR, null, $"Review [{ItemId}]: {b.GetErrorsAsString()}");
return false;
}
}
}
//States collection
if (!await StateDeleteAsync(id, transaction))
return false;
//collect the child id's to delete
var ItemIds = await ct.QuoteItem.AsNoTracking().Where(z => z.QuoteId == id).Select(z => z.Id).ToListAsync();
//Delete children
foreach (long ItemId in ItemIds)
if (!await ItemDeleteAsync(ItemId, transaction))
return false;
ct.Quote.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 QuoteHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task QuoteBizActionsAsync(AyaEvent ayaEvent, Quote newObj, Quote 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;
//CREATED OR MODIFIED
if (ayaEvent == AyaEvent.Created || ayaEvent == AyaEvent.Modified)
{
}
//CREATION ACTIONS
if (ayaEvent == AyaEvent.Created)
{
await AutoSetContractAsync(newObj);
await AutoSetAddressAsync(newObj);
}
//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);
}
}
}
private async Task AutoSetAddressAsync(Quote newObj)
{
if (!string.IsNullOrWhiteSpace(newObj.PostAddress) || !string.IsNullOrWhiteSpace(newObj.Address))
return;
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(Quote 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;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET WORKORDER ID FROM DESCENDANT TYPE AND ID
//
internal static async Task<ParentAndChildItemId> GetQuoteIdFromRelativeAsync(AyaType ayaType, long id, AyContext ct)
{
ParentAndChildItemId w = new ParentAndChildItemId();
long itemid = 0;
switch (ayaType)
{
case AyaType.Quote:
w.ParentId = id;
w.ChildItemId = 0;
return w;
case AyaType.QuoteItem:
itemid = id;
break;
case AyaType.QuoteItemExpense:
itemid = await ct.QuoteItemExpense.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
case AyaType.QuoteItemLabor:
itemid = await ct.QuoteItemLabor.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
case AyaType.QuoteItemLoan:
itemid = await ct.QuoteItemLoan.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
case AyaType.QuoteItemPart:
itemid = await ct.QuoteItemPart.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
case AyaType.QuoteItemScheduledUser:
itemid = await ct.QuoteItemScheduledUser.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
case AyaType.QuoteItemTask:
itemid = await ct.QuoteItemTask.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
case AyaType.QuoteItemTravel:
itemid = await ct.QuoteItemTravel.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
case AyaType.QuoteItemOutsideService:
itemid = await ct.QuoteItemOutsideService.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
case AyaType.QuoteStatus:
w.ParentId = await ct.QuoteState.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteId).SingleOrDefaultAsync();
w.ChildItemId = 0;
return w;
case AyaType.QuoteItemUnit:
itemid = await ct.QuoteItemUnit.AsNoTracking().Where(z => z.Id == id).Select(z => z.QuoteItemId).SingleOrDefaultAsync();
break;
default:
throw new System.NotSupportedException($"QuoteBiz::GetQuoteIdFromRelativeAsync -> AyaType {ayaType.ToString()} is not supported");
}
w.ParentId = await ct.QuoteItem.AsNoTracking()
.Where(z => z.Id == itemid)
.Select(z => z.QuoteId)
.SingleOrDefaultAsync();
w.ChildItemId = itemid;
return w;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//SEARCH
//
private async Task QuoteSearchIndexAsync(Quote 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, AyaType specificType)
{
switch (specificType)
{
case AyaType.Quote:
var obj = await ct.Quote.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);//# NOTE intentionally not calling quote get async here, don't need the whole graph
var SearchParams = new Search.SearchIndexProcessObjectParameters();
DigestSearchText(obj, SearchParams);
return SearchParams;
case AyaType.QuoteItem:
return await ItemGetSearchResultSummary(id);
case AyaType.QuoteItemExpense:
return await ExpenseGetSearchResultSummary(id);
case AyaType.QuoteItemLabor:
return await LaborGetSearchResultSummary(id);
case AyaType.QuoteItemLoan:
return await LoanGetSearchResultSummary(id);
case AyaType.QuoteItemPart:
return await PartGetSearchResultSummary(id);
case AyaType.QuoteItemTask:
return await TaskGetSearchResultSummary(id);
case AyaType.QuoteItemTravel:
return await TravelGetSearchResultSummary(id);
case AyaType.QuoteItemOutsideService:
return await OutsideServiceGetSearchResultSummary(id);
case AyaType.QuoteItemUnit:
return await UnitGetSearchResultSummary(id);
default:
return null;
}
}
public void DigestSearchText(Quote obj, Search.SearchIndexProcessObjectParameters searchParams)
{
if (obj != null)
searchParams.AddText(obj.Notes)
.AddText(obj.Introduction)
.AddText(obj.Serial)
.AddText(obj.InternalReferenceNumber)
.AddText(obj.CustomerReferenceNumber)
.AddText(obj.CustomerContactName)
.AddText(obj.PostAddress)
.AddText(obj.PostCity)
.AddText(obj.PostRegion)
.AddText(obj.PostCountry)
.AddText(obj.PostCode)
.AddText(obj.Address)
.AddText(obj.City)
.AddText(obj.Region)
.AddText(obj.Country)
.AddText(obj.Wiki)
.AddText(obj.Tags)
.AddCustomFields(obj.CustomFields);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
//Can save or update?
private async Task QuoteValidateAsync(Quote proposedObj, Quote currentObj)
{
//This may become necessary for v8migrate, leaving out for now
//skip validation if seeding
//if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
//Check restricted role preventing create
if (isNew && UserIsRestrictedType)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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 async Task QuoteValidateCanDelete(Quote dbObject)
{
//Check restricted role preventing create
if (UserIsRestrictedType)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;//this is a completely disqualifying error
}
if (await ct.WorkOrder.AnyAsync(m => m.FromQuoteId == dbObject.Id))
AddError(ApiErrorCode.VALIDATION_REFERENTIAL_INTEGRITY, "generalerror", await Translate("Quote"));
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET PARTIAL WORKORDER FOR REPORTING
// (returns quote consisting only of the path from child or grandchild up to header populated
// with display data for reporting)
//
internal async Task<Quote> QuoteGetPartialAsync(AyaType ayaType, long id, bool includeWoItemDescendants, bool populateForReporting)
{
//if it's the entire quote just get, populate and return as normal
if (ayaType == AyaType.Quote)
return await QuoteGetAsync(id, true, false, populateForReporting);
var wid = await GetQuoteIdFromRelativeAsync(ayaType, id, ct);
//get header only
var ret = await ct.Quote.AsNoTracking().Include(s => s.States).SingleOrDefaultAsync(x => x.Id == wid.ParentId);
//not found don't bomb, just return null
if (ret == null) return ret;
//explicit load subitems as required...
QuoteItem quoteitem = null;
//it's requesting a fully populated woitem so do that here
if (includeWoItemDescendants)
{
quoteitem = await ct.QuoteItem.AsSplitQuery()
.AsNoTracking()
.Include(wi => wi.Expenses)
.Include(wi => wi.Labors)
.Include(wi => wi.Loans)
.Include(wi => wi.Parts)
.Include(wi => wi.ScheduledUsers)
.Include(wi => wi.Tasks)
.Include(wi => wi.Travels)
.Include(wi => wi.Units)
.Include(wi => wi.OutsideServices)
.SingleOrDefaultAsync(z => z.Id == wid.ChildItemId);
}
else
{
//get the single quote item required
quoteitem = await ct.QuoteItem.AsNoTracking().SingleOrDefaultAsync(x => x.Id == wid.ChildItemId);
switch (ayaType)
{
case AyaType.QuoteItemExpense:
quoteitem.Expenses.Add(await ct.QuoteItemExpense.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
case AyaType.QuoteItemLabor:
quoteitem.Labors.Add(await ct.QuoteItemLabor.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
case AyaType.QuoteItemLoan:
quoteitem.Loans.Add(await ct.QuoteItemLoan.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
case AyaType.QuoteItemPart:
quoteitem.Parts.Add(await ct.QuoteItemPart.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
case AyaType.QuoteItemScheduledUser:
quoteitem.ScheduledUsers.Add(await ct.QuoteItemScheduledUser.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
case AyaType.QuoteItemTask:
quoteitem.Tasks.Add(await ct.QuoteItemTask.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
case AyaType.QuoteItemTravel:
quoteitem.Travels.Add(await ct.QuoteItemTravel.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
case AyaType.QuoteItemOutsideService:
quoteitem.OutsideServices.Add(await ct.QuoteItemOutsideService.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
case AyaType.QuoteItemUnit:
quoteitem.Units.Add(await ct.QuoteItemUnit.AsNoTracking().Where(z => z.Id == id).SingleOrDefaultAsync());
break;
}
}
if (quoteitem != null)
ret.Items.Add(quoteitem);
await QuotePopulateVizFields(ret, false, populateForReporting);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//REPORTING
//
public async Task<JArray> GetReportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//quote reports for entire quote or just sub parts all go through here
//if the ayatype is a descendant of the quote then only the portion of the quote from that descendant directly up to the header will be populated and returned
//however if the report template has includeWoItemDescendants=true then the woitems is fully populated
var idList = dataListSelectedRequest.SelectedRowIds;
JArray ReportData = new JArray();
List<Quote> batchResults = new List<Quote>();
while (idList.Any())
{
var batch = idList.Take(IReportAbleObject.REPORT_DATA_BATCH_SIZE);
idList = idList.Skip(IReportAbleObject.REPORT_DATA_BATCH_SIZE).ToArray();
batchResults.Clear();
foreach (long batchId in batch)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
batchResults.Add(await QuoteGetPartialAsync(dataListSelectedRequest.AType, batchId, dataListSelectedRequest.IncludeWoItemDescendants, true));
}
foreach (Quote w in batchResults)
{
if (!ReportRenderManager.KeepGoing(jobId)) return null;
var jo = JObject.FromObject(w);
//Quote header custom fields
if (!JsonUtil.JTokenIsNullOrEmpty(jo["CustomFields"]))
jo["CustomFields"] = JObject.Parse((string)jo["CustomFields"]);
//QuoteItem custom fields
foreach (JObject jItem in jo["Items"])
{
if (!JsonUtil.JTokenIsNullOrEmpty(jItem["CustomFields"]))
jItem["CustomFields"] = JObject.Parse((string)jItem["CustomFields"]);
//QuoteItemUnit custom fields
foreach (JObject jUnit in jItem["Units"])
{
if (!JsonUtil.JTokenIsNullOrEmpty(jUnit["CustomFields"]))
jUnit["CustomFields"] = JObject.Parse((string)jUnit["CustomFields"]);
}
}
ReportData.Add(jo);
}
}
vc.Clear();
oc.Clear();
return ReportData;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task QuotePopulateVizFields(Quote o, bool headerOnly, bool populateForReporting)
{
o.UserIsRestrictedType = UserIsRestrictedType;
o.UserIsTechRestricted = UserIsTechRestricted;
o.UserIsSubContractorFull = UserIsSubContractorFull;
o.UserIsSubContractorRestricted = UserIsSubContractorRestricted;
o.UserCanViewPartCosts = UserCanViewPartCosts;
o.UserCanViewLaborOrTravelRateCosts = UserCanViewLaborOrTravelRateCosts;
o.UserCanViewLoanerCosts = UserCanViewLoanerCosts;
if (!headerOnly)
{
foreach (var v in o.States)
await StatePopulateVizFields(v);
foreach (var v in o.Items)
await ItemPopulateVizFields(v, populateForReporting);
}
//Alert notes
//Customer notes first then others below
{
if (vc.Get("wocustname", o.CustomerId) == null)//will always be present so no need to check other values
{
var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == o.CustomerId).Select(x => new { x.AlertNotes, x.TechNotes, x.Name, x.Phone1, x.Phone2, x.Phone3, x.Phone4, x.Phone5, x.EmailAddress }).FirstOrDefaultAsync();
vc.Add(custInfo.Name, "wocustname", o.CustomerId);
if (!string.IsNullOrWhiteSpace(custInfo.AlertNotes))
{
vc.Add($"{await Translate("Customer")} - {await Translate("AlertNotes")}\n{custInfo.AlertNotes}\n\n", "woalert", o.CustomerId);
}
else
{
vc.Add(string.Empty, "woalert", o.CustomerId);
}
if (!string.IsNullOrWhiteSpace(custInfo.TechNotes))
{
vc.Add(custInfo.TechNotes, "custtechnotes", o.CustomerId);
}
vc.Add(custInfo.Phone1, "custphone1", o.CustomerId);
vc.Add(custInfo.Phone2, "custphone2", o.CustomerId);
vc.Add(custInfo.Phone3, "custphone3", o.CustomerId);
vc.Add(custInfo.Phone4, "custphone4", o.CustomerId);
vc.Add(custInfo.Phone5, "custphone5", o.CustomerId);
vc.Add(custInfo.EmailAddress, "custemail", o.CustomerId);
}
o.CustomerViz = vc.Get("wocustname", o.CustomerId);
o.AlertViz = vc.Get("woalert", o.CustomerId);
o.CustomerTechNotesViz = vc.Get("custtechnotes", o.CustomerId);
o.CustomerPhone1Viz = vc.Get("custphone1", o.CustomerId);
o.CustomerPhone2Viz = vc.Get("custphone2", o.CustomerId);
o.CustomerPhone3Viz = vc.Get("custphone3", o.CustomerId);
o.CustomerPhone4Viz = vc.Get("custphone4", o.CustomerId);
o.CustomerPhone5Viz = vc.Get("custphone5", o.CustomerId);
o.CustomerEmailAddressViz = vc.Get("custemail", o.CustomerId);
}
if (o.ProjectId != null)
{
string value = vc.Get("projname", o.ProjectId);
if (value == null)
{
value = await ct.Project.AsNoTracking().Where(x => x.Id == o.ProjectId).Select(x => x.Name).FirstOrDefaultAsync();
vc.Add(value, "projname", o.ProjectId);
}
o.ProjectViz = value;
}
if (o.PreparedById != null)
{
if (!vc.Has("user", o.PreparedById))
{
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.PreparedById).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.PreparedById);
}
o.PreparedByViz = vc.Get("user", o.PreparedById);
}
if (o.ContractId != null)
{
if (vc.Get("ctrctname", 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();
vc.Add(contractVizFields.Name, "ctrctname", o.ContractId);
vc.Add(contractVizFields.AlertNotes, "ctrctalrt", o.ContractId);
}
o.ContractViz = vc.Get("ctrctname", o.ContractId);//contractVizFields.Name;
var alrtNotes = vc.Get("ctrctalrt", o.ContractId);
if (!string.IsNullOrWhiteSpace(alrtNotes))
{
o.AlertViz += $"{await Translate("Contract")}\n{alrtNotes}\n\n";
}
}
else
o.ContractViz = "-";
if (o.LastStatusId != null && o.States.Count > 0)
{
var lastState = o.States[o.States.Count - 1];
o.LastStateColorViz = lastState.ColorViz;
o.LastStateCompletedViz = lastState.CompletedViz;
o.LastStateLockedViz = lastState.LockedViz;
o.LastStateNameViz = lastState.NameViz;
o.LastStateUserViz = lastState.UserViz;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// IMPORT EXPORT
//
public async Task<JArray> GetExportData(DataListSelectedRequest dataListSelectedRequest, Guid jobId)
{
//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(dataListSelectedRequest, jobId);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//JOB / OPERATIONS
//
public async Task HandleJobAsync(OpsJob job)
{
switch (job.JobType)
{
case JobType.BatchCoreObjectOperation:
await ProcessBatchJobAsync(job);
break;
default:
throw new System.ArgumentOutOfRangeException($"Quote.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.Quote.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 GetQuoteGraphItem(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 DeleteQuoteGraphItem(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 PutQuoteGraphItem(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 QuoteHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{this.BizType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
Quote oProposed = (Quote)proposedObj;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);//Note: will properly handle all delete events and event removal if deleted
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion quote level
/*
███████╗████████╗ █████╗ ████████╗███████╗███████╗
██╔════╝╚══██╔══╝██╔══██╗╚══██╔══╝██╔════╝██╔════╝
███████╗ ██║ ███████║ ██║ █████╗ ███████╗
╚════██║ ██║ ██╔══██║ ██║ ██╔══╝ ╚════██║
███████║ ██║ ██║ ██║ ██║ ███████╗███████║
╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝
*/
#region QuoteState level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> StateExistsAsync(long id)
{
return await ct.QuoteState.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteState> StateCreateAsync(QuoteState newObject)
{
await StateValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.QuoteState.AddAsync(newObject);
var qoute = await ct.Quote.FirstOrDefaultAsync(x => x.Id == newObject.QuoteId);
var newStatusInfo = await ct.QuoteStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == newObject.QuoteStatusId);
qoute.LastStatusId = newObject.QuoteStatusId;
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.QuoteStatus, AyaEvent.Created), ct);
await StateHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteState> StateGetAsync(long id, bool logTheGetEvent = true)
{
//Note: there could be rules checking here in future, i.e. can only get own quote 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.QuoteState.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.QuoteStatus, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task StatePopulateVizFields(QuoteState o)
{
if (o.UserId != 0)
{
if (!vc.Has("user", o.UserId))
{
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
}
o.UserViz = vc.Get("user", o.UserId);
}
QuoteStatus QStatus = null;
if (!oc.Has("quotestatus", o.QuoteStatusId))
{
QStatus = await ct.QuoteStatus.AsNoTracking().Where(x => x.Id == o.QuoteStatusId).FirstOrDefaultAsync();
oc.Add(QStatus, "quotestatus", o.QuoteStatusId);
}
else
QStatus = (QuoteStatus)oc.Get("quotestatus", o.QuoteStatusId);
o.NameViz = QStatus.Name;
o.ColorViz = QStatus.Color;
o.CompletedViz = QStatus.Completed;
o.LockedViz = QStatus.Locked;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
// (note: this would only ever be called when a quote is deleted, there is no direct delete)
internal async Task<bool> StateDeleteAsync(long workOrderId, IDbContextTransaction parentTransaction)
{
var stateList = await ct.QuoteState.AsNoTracking().Where(z => z.QuoteId == workOrderId).ToListAsync();
foreach (var wostate in stateList)
{
ct.QuoteState.Remove(wostate);
await ct.SaveChangesAsync();
//no need to call this because it's only going to run this method if the quote is deleted and
//via process standard notifciation events for quote deletion will remove any state delayed notifications anyway so
//nothing to call or do here related to notification
// await StateHandlePotentialNotificationEvent(AyaEvent.Deleted, wostate);
}
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task StateValidateAsync(QuoteState proposedObj, QuoteState currentObj)
{
//of all restricted users, only a restricted tech can change status
if (UserIsSubContractorFull || UserIsSubContractorRestricted)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
//run validation and biz rules
bool isNew = currentObj == null;
//does it have a valid quote id
if (proposedObj.QuoteId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteId");
else if (!await QuoteExistsAsync(proposedObj.QuoteId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteId");
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task StateHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteState oProposed = (QuoteState)proposedObj;
var QuoteInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == oProposed.QuoteId).Select(x => new { x.Serial, x.Tags, x.CustomerId }).FirstOrDefaultAsync();
QuoteStatus qos = await ct.QuoteStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == oProposed.QuoteStatusId);
//for notification purposes because has no name / tags field itself
oProposed.Name = QuoteInfo.Serial.ToString();
oProposed.Tags = QuoteInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
//NONE: state notifications are specific and not the same as for general objects so don't process standard events
//SPECIFIC EVENTS FOR THIS OBJECT
//QuoteStatusChange = 9,//* Quote object, any *change* of status including from no status (new) to a specific conditional status ID value
// QuoteStatusAge = 29,//* Quote object Created / Updated, conditional on exact status selected IdValue, Tags conditional, advance notice can be set
//NOTE: ID, state notifications are for the Quote, not the state itself unlike other objects, so use the quote type and ID here for all notifications
//## DELETED EVENTS
//A state cannot be deleted so nothing to handle that is required
//a quote CAN be deleted and it will automatically remove all events for it so also no need to remove time delayed status events either if wo is deleted.
//so in essence there is nothing to be done regarding deleted events with states in a blanket way, however specific events below may remove them as appropriate
//## CREATED (this is the only possible notification CREATION ayaEvent type for a quote state as they are create only)
if (ayaEvent == AyaEvent.Created)
{
//# STATUS CHANGE (create new status)
{
//PERSONAL SUBSCCRIPTION
//Conditions: must match specific status id value and also tags below
//delivery is immediate so no need to remove old ones of this kind
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.QuoteStatusChange && z.IdValue == oProposed.QuoteStatusId).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Tag match? (will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(QuoteInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.QuoteStatusChange,
UserId = sub.UserId,
AyaType = AyaType.Quote,
ObjectId = oProposed.QuoteId,
NotifySubscriptionId = sub.Id,
Name = $"{QuoteInfo.Serial.ToString()} - {qos.Name}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//quote status change event
{
//PROXY CUSTOMER NOTIFICATION SUBSCRIPTION HANDLING
//can this customer even be delivered to?
var custInfo = await ct.Customer.AsNoTracking().Where(x => x.Id == QuoteInfo.CustomerId).Select(x => new { x.Active, x.Tags, x.EmailAddress }).FirstOrDefaultAsync();
if (custInfo != null && custInfo.Active && !string.IsNullOrWhiteSpace(custInfo.EmailAddress))
{
//Conditions: must match specific status id value and also tags below
//delivery is immediate so no need to remove old ones of this kind
//note order by id ascending so that only the oldest notification "wins" as per docs in case of overlap to same customer
var subs = await ct.CustomerNotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.QuoteStatusChange && z.IdValue == oProposed.QuoteStatusId).OrderBy(z => z.Id).ToListAsync();
foreach (var sub in subs)
{
//Object tags must match and Customer tags must match
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(QuoteInfo.Tags, sub.Tags) && NotifyEventHelper.ObjectHasAllSubscriptionTags(custInfo.Tags, sub.CustomerTags))
{
CustomerNotifyEvent n = new CustomerNotifyEvent()
{
EventType = NotifyEventType.QuoteStatusChange,
CustomerId = QuoteInfo.CustomerId,
AyaType = AyaType.Quote,
ObjectId = oProposed.QuoteId,
CustomerNotifySubscriptionId = sub.Id,
Name = QuoteInfo.Serial.ToString(),
Subject="TEST SUBJECT",
Message="TEST MESSAGE"//TODO: template processing here
};
await ct.CustomerNotifyEvent.AddAsync(n);
log.LogDebug($"Adding CustomerNotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
break;//we have a match no need to process any further subs for this event
}
}
}
}//quote status change event
//# STATUS AGE
{
//QuoteStatusAge = 29,//* Quote STATUS unchanged for set time (stuck in state), conditional on: Duration (how long stuck), exact status selected IdValue, Tags. Advance notice can NOT be set
//Always clear any old ones for this object as they are all irrelevant the moment the state has changed:
await NotifyEventHelper.ClearPriorEventsForObject(ct, proposedObj.AyaType, proposedObj.Id, NotifyEventType.QuoteStatusAge);
var subs = await ct.NotifySubscription.AsNoTracking().Where(z => z.EventType == NotifyEventType.QuoteStatusAge && z.IdValue == oProposed.QuoteStatusId).ToListAsync();
foreach (var sub in subs)
{
//not for inactive users
if (!await UserBiz.UserIsActive(sub.UserId)) continue;
//Quote Tag match? (Not State, state has no tags, will be true if no sub tags so always safe to call this)
if (NotifyEventHelper.ObjectHasAllSubscriptionTags(QuoteInfo.Tags, sub.Tags))
{
NotifyEvent n = new NotifyEvent()
{
EventType = NotifyEventType.QuoteStatusAge,
UserId = sub.UserId,
AyaType = AyaType.Quote,
ObjectId = oProposed.QuoteId,
NotifySubscriptionId = sub.Id,
Name = $"{QuoteInfo.Serial.ToString()} - {qos.Name}"
};
await ct.NotifyEvent.AddAsync(n);
log.LogDebug($"Adding NotifyEvent: [{n.ToString()}]");
await ct.SaveChangesAsync();
}
}
}//quote status age event
}
}//end of process notifications
#endregion work order STATE level
/*
██╗████████╗███████╗███╗ ███╗███████╗
██║╚══██╔══╝██╔════╝████╗ ████║██╔════╝
██║ ██║ █████╗ ██╔████╔██║███████╗
██║ ██║ ██╔══╝ ██║╚██╔╝██║╚════██║
██║ ██║ ███████╗██║ ╚═╝ ██║███████║
╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝
*/
#region QuoteItem level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ItemExistsAsync(long id)
{
return await ct.QuoteItem.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItem> ItemCreateAsync(QuoteItem newObject)
{
await ItemValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.QuoteItem.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, AyaType.QuoteItem, AyaEvent.Created), ct);
await ItemSearchIndexAsync(newObject, true);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, newObject.Tags, null);
await ItemPopulateVizFields(newObject, false);
await ItemHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItem> ItemGetAsync(long id, bool logTheGetEvent = true)
{
//Restricted users can not fetch a woitem directly
//arbitrary decision so don't have to put in all the cleanup code
//because from our own UI they wouldn't fetch this anyway and
//so this is only to cover api use by 3rd parties
if (UserIsRestrictedType)
{
return null;
}
//Note: there could be rules checking here in future, i.e. can only get own quote 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.QuoteItem.AsSplitQuery().AsNoTracking()
.Include(wi => wi.Expenses)
.Include(wi => wi.Labors)
.Include(wi => wi.Loans)
.Include(wi => wi.Parts)
.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.QuoteItem, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<QuoteItem> ItemPutAsync(QuoteItem 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.QuoteItem.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.QuoteItem, AyaEvent.Modified), ct);
await ItemSearchIndexAsync(putObject, false);
await TagBiz.ProcessUpdateTagsInRepositoryAsync(ct, putObject.Tags, dbObject.Tags);
await ItemPopulateVizFields(putObject, false);
await ItemHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ItemDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await ct.QuoteItem.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.QuoteItemExpense.Where(z => z.QuoteItemId == id).Select(z => z.Id).ToListAsync();
var LaborIds = await ct.QuoteItemLabor.Where(z => z.QuoteItemId == id).Select(z => z.Id).ToListAsync();
var LoanIds = await ct.QuoteItemLoan.Where(z => z.QuoteItemId == id).Select(z => z.Id).ToListAsync();
var PartIds = await ct.QuoteItemPart.Where(z => z.QuoteItemId == id).Select(z => z.Id).ToListAsync();
var ScheduledUserIds = await ct.QuoteItemScheduledUser.Where(z => z.QuoteItemId == id).Select(z => z.Id).ToListAsync();
var TaskIds = await ct.QuoteItemTask.Where(z => z.QuoteItemId == id).Select(z => z.Id).ToListAsync();
var TravelIds = await ct.QuoteItemTravel.Where(z => z.QuoteItemId == id).Select(z => z.Id).ToListAsync();
var UnitIds = await ct.QuoteItemUnit.Where(z => z.QuoteItemId == id).Select(z => z.Id).ToListAsync();
var OutsideServiceIds = await ct.QuoteItemOutsideService.Where(z => z.QuoteItemId == 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 ScheduledUserIds)
if (!await ScheduledUserDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in TaskIds)
if (!await TaskDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in TravelIds)
if (!await TravelDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in UnitIds)
if (!await UnitDeleteAsync(ItemId, transaction))
return false;
foreach (long ItemId in OutsideServiceIds)
if (!await OutsideServiceDeleteAsync(ItemId, transaction))
return false;
ct.QuoteItem.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "wo:" + dbObject.QuoteId.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);
return true;
}
private async Task ItemSearchIndexAsync(QuoteItem obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, AyaType.QuoteItem);
SearchParams.AddText(obj.Notes).AddText(obj.TechNotes).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.QuoteItem.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.TechNotes).AddText(obj.Wiki).AddText(obj.Tags).AddCustomFields(obj.CustomFields);
return SearchParams;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task ItemPopulateVizFields(QuoteItem o, bool populateForReporting)
{
if (o.WorkOrderItemStatusId != null)
{
string value = vc.Get("woistatname", o.WorkOrderItemStatusId);
if (value == null)
{
var StatusInfo = await ct.WorkOrderItemStatus.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.WorkOrderItemStatusId);
vc.Add(StatusInfo.Name, "woistatname", o.WorkOrderItemStatusId);
vc.Add(StatusInfo.Color, "woistatcolor", o.WorkOrderItemStatusId);
o.WorkOrderItemStatusNameViz = StatusInfo.Name;
o.WorkOrderItemStatusColorViz = StatusInfo.Color;
}
else
{
o.WorkOrderItemStatusNameViz = value;
o.WorkOrderItemStatusColorViz = vc.Get("woistatcolor", o.WorkOrderItemStatusId);
}
}
if (o.WorkOrderItemPriorityId != null)
{
string value = vc.Get("woipriorityname", o.WorkOrderItemPriorityId);
if (value == null)
{
var PriorityInfo = await ct.WorkOrderItemPriority.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.WorkOrderItemPriorityId);
vc.Add(PriorityInfo.Name, "woipriorityname", o.WorkOrderItemPriorityId);
vc.Add(PriorityInfo.Color, "woiprioritycolor", o.WorkOrderItemPriorityId);
o.WorkOrderItemPriorityNameViz = PriorityInfo.Name;
o.WorkOrderItemPriorityColorViz = PriorityInfo.Color;
}
else
{
o.WorkOrderItemPriorityNameViz = value;
o.WorkOrderItemPriorityColorViz = vc.Get("woiprioritycolor", o.WorkOrderItemPriorityId);
}
}
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.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, populateForReporting);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ItemValidateAsync(QuoteItem proposedObj, QuoteItem currentObj)
{
//run validation and biz rules
bool isNew = currentObj == null;
//does it have a valid quote id
if (proposedObj.QuoteId == 0)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteId");
else if (!await QuoteExistsAsync(proposedObj.QuoteId))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteId");
//Check restricted role preventing create
if (isNew && UserIsRestrictedType)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (string.IsNullOrWhiteSpace(proposedObj.Notes))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Notes");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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(QuoteItem obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
//Check restricted role preventing create
if (UserIsRestrictedType)
{
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;//this is a completely disqualifying error
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.QuoteItem))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task ItemHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItem oProposed = (QuoteItem)proposedObj;
var qid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == qid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
//for notification purposes because has no name field itself
if (WorkorderInfo != null)
oProposed.Name = WorkorderInfo.Serial.ToString();
else
oProposed.Name = "??";
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item level
/*
███████╗██╗ ██╗██████╗ ███████╗███╗ ██╗███████╗███████╗███████╗
██╔════╝╚██╗██╔╝██╔══██╗██╔════╝████╗ ██║██╔════╝██╔════╝██╔════╝
█████╗ ╚███╔╝ ██████╔╝█████╗ ██╔██╗ ██║███████╗█████╗ ███████╗
██╔══╝ ██╔██╗ ██╔═══╝ ██╔══╝ ██║╚██╗██║╚════██║██╔══╝ ╚════██║
███████╗██╔╝ ██╗██║ ███████╗██║ ╚████║███████║███████╗███████║
╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚══════╝╚══════╝
*/
#region QuoteItemExpense level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ExpenseExistsAsync(long id)
{
return await ct.QuoteItemExpense.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemExpense> ExpenseCreateAsync(QuoteItemExpense newObject)
{
await ExpenseValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.QuoteItemExpense.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await ExpenseSearchIndexAsync(newObject, true);
await ExpensePopulateVizFields(newObject);
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemExpense> ExpenseGetAsync(long id, bool logTheGetEvent = true)
{
if (UserIsSubContractorFull || UserIsSubContractorRestricted) //no access allowed at all
return null;
var ret = await ct.QuoteItemExpense.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (UserIsTechRestricted && ret.UserId != UserId)//tech restricted can only see their own expenses
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return null;
}
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<QuoteItemExpense> ExpensePutAsync(QuoteItemExpense 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;
}
await ExpenseValidateAsync(putObject, dbObject);
if (HasErrors) return 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 ExpensePopulateVizFields(putObject);
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ExpenseDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await ExpenseGetAsync(id, false);
ExpenseValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.QuoteItemExpense.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await ExpenseHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task ExpenseSearchIndexAsync(QuoteItemExpense 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(QuoteItemExpense o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.UserId != null)
{
if (!vc.Has("user", o.UserId))
{
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
}
o.UserViz = vc.Get("user", o.UserId);
}
}
TaxCode Tax = null;
if (o.ChargeTaxCodeId != null)
{
if (!oc.Has("tax", o.ChargeTaxCodeId))
{
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.ChargeTaxCodeId);
oc.Add(Tax, "tax", o.ChargeTaxCodeId);
}
else
Tax = (TaxCode)oc.Get("tax", o.ChargeTaxCodeId);
}
if (Tax != null)
o.TaxCodeViz = Tax.Name;
//Calculate totals and taxes
if (o.ChargeToCustomer)
{
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = MoneyUtil.Round(o.ChargeAmount * (Tax.TaxAPct / 100));
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = MoneyUtil.Round((o.ChargeAmount + o.TaxAViz) * (Tax.TaxBPct / 100));
}
else
{
o.TaxBViz = MoneyUtil.Round(o.ChargeAmount * (Tax.TaxBPct / 100));
}
}
o.LineTotalViz = o.ChargeAmount + o.TaxAViz + o.TaxBViz;
}
else
{
o.LineTotalViz = o.ChargeAmount + o.TaxPaid;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ExpenseValidateAsync(QuoteItemExpense proposedObj, QuoteItemExpense currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (UserIsSubContractorFull || UserIsSubContractorRestricted)
{
//no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId)))
{
//no edits allowed on other people's records
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (!isNew && UserIsTechRestricted)
{
//Existing record so just make sure they haven't changed the not changeable fields from the db version
//Expenses: add (no user selection defaults to themselves), view, partial fields available
// to edit or delete only where they are the selected user and only edit fields
//Summary, Cost, Tax paid, Description
//note that UI will prevent this, this rule is only backup for 3rd party api users
if (currentObj.ChargeAmount != proposedObj.ChargeAmount) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeAmount");
//if (currentObj.TaxPaid != proposedObj.TaxPaid) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "TaxPaid");
if (currentObj.ChargeTaxCodeId != proposedObj.ChargeTaxCodeId) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeTaxCodeId");
if (currentObj.ReimburseUser != proposedObj.ReimburseUser) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReimburseUser");
if (currentObj.ChargeToCustomer != proposedObj.ChargeToCustomer) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeToCustomer");
}
if (isNew && UserIsTechRestricted)
{
//NEW record, they are not allowed to set several fields so make sure they are still at their defaults
/* from client new expense record:
concurrency: 0,
description: null,
name: null,
totalCost: 0,
chargeAmount: 0,
taxPaid: 0,
chargeTaxCodeId: null,
taxCodeViz: null,
reimburseUser: false,
userId: null,
userViz: null,
chargeToCustomer: false,
isDirty: true,
workOrderItemId: this.value.items[this.activeWoItemIndex].id,
uid: Date.now() //used for
*/
if (proposedObj.ChargeAmount != 0) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeAmount");
// if (proposedObj.TaxPaid != 0) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "TaxPaid");
if (proposedObj.ChargeTaxCodeId != null) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeTaxCodeId");
if (proposedObj.ReimburseUser != false) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ReimburseUser");
if (proposedObj.ChargeToCustomer != false) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "ChargeToCustomer");
}
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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(QuoteItemExpense obj)
{
if (UserIsSubContractorFull || UserIsSubContractorRestricted)
{
//no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
if (UserIsTechRestricted && obj.UserId != UserId)
{
//no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.QuoteItemExpense))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task ExpenseHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemExpense oProposed = (QuoteItemExpense)proposedObj;
var qid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == qid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item EXPENSE level
/*
██╗ █████╗ ██████╗ ██████╗ ██████╗
██║ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗
██║ ███████║██████╔╝██║ ██║██████╔╝
██║ ██╔══██║██╔══██╗██║ ██║██╔══██╗
███████╗██║ ██║██████╔╝╚██████╔╝██║ ██║
╚══════╝╚═╝ ╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝
*/
#region QuoteItemLabor level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> LaborExistsAsync(long id)
{
return await ct.QuoteItemLabor.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemLabor> LaborCreateAsync(QuoteItemLabor newObject)
{
await LaborValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.QuoteItemLabor.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await LaborSearchIndexAsync(newObject, true);
await LaborPopulateVizFields(newObject);
await LaborHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemLabor> LaborGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.QuoteItemLabor.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (UserIsRestrictedType && ret.UserId != UserId)
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return null;
}
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<QuoteItemLabor> LaborPutAsync(QuoteItemLabor 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;
}
await LaborValidateAsync(putObject, dbObject);
if (HasErrors) return null;
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await LaborExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await LaborSearchIndexAsync(putObject, false);
await LaborPopulateVizFields(putObject);
await LaborHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> LaborDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await LaborGetAsync(id, false);
LaborValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.QuoteItemLabor.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await LaborHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task LaborSearchIndexAsync(QuoteItemLabor 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(QuoteItemLabor o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.UserId != null)
{
if (!vc.Has("user", o.UserId))
{
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
}
o.UserViz = vc.Get("user", o.UserId);
}
}
ServiceRate Rate = null;
if (o.ServiceRateId != null)
{
if (!oc.Has("servicerate", o.ServiceRateId))
{
Rate = await ct.ServiceRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.ServiceRateId);
oc.Add(Rate, "servicerate", o.ServiceRateId);
}
else
Rate = (ServiceRate)oc.Get("servicerate", o.ServiceRateId);
o.ServiceRateViz = Rate.Name;
}
TaxCode Tax = null;
if (o.TaxCodeSaleId != null)
{
if (!oc.Has("tax", o.TaxCodeSaleId))
{
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
oc.Add(Tax, "tax", o.TaxCodeSaleId);
}
else
Tax = (TaxCode)oc.Get("tax", o.TaxCodeSaleId);
}
if (Tax != null)
o.TaxCodeViz = 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 GetCurrentQuoteContractFromRelatedAsync(AyaType.QuoteItem, o.QuoteItemId);
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 && Rate != null)
{
//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 = MoneyUtil.Round(o.CostViz + (o.CostViz * pct));
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = MoneyUtil.Round(o.ListPriceViz - (o.ListPriceViz * pct));
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = MoneyUtil.Round(o.PriceViz * o.ServiceRateQuantity);
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
}
else
{
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
//RESTRICTIONS ON COST VISIBILITY?
if (!UserCanViewLaborOrTravelRateCosts)
{
o.CostViz = 0;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task LaborValidateAsync(QuoteItemLabor proposedObj, QuoteItemLabor currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId)))
{
//no edits allowed on other people's records
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
//Start date AND end date must both be null or both contain values
if (proposedObj.ServiceStartDate == null && proposedObj.ServiceStopDate != null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServiceStartDate");
if (proposedObj.ServiceStartDate != null && proposedObj.ServiceStopDate == null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "ServiceStopDate");
//Start date before end date
if (proposedObj.ServiceStartDate != null && proposedObj.ServiceStopDate != null)
if (proposedObj.ServiceStartDate > proposedObj.ServiceStopDate)
AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "ServiceStartDate");
if (proposedObj.ServiceRateQuantity < 0)//negative quantities are not allowed
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "ServiceRateQuantity");
if (proposedObj.NoChargeQuantity < 0)//negative quantities are not allowed
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "NoChargeQuantity");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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
}
}
private void LaborValidateCanDelete(QuoteItemLabor obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
if (UserIsRestrictedType)
{
//Labors: add (no user selection defaults to themselves), remove, view and edit only when they are the selected User
if (obj.UserId != UserId)
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.QuoteItemLabor))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task LaborHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemLabor oProposed = (QuoteItemLabor)proposedObj;
var wid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
//for notification purposes because has no name or tags field itself
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item LABOR level
/*
██╗ ██████╗ █████╗ ███╗ ██╗
██║ ██╔═══██╗██╔══██╗████╗ ██║
██║ ██║ ██║███████║██╔██╗ ██║
██║ ██║ ██║██╔══██║██║╚██╗██║
███████╗╚██████╔╝██║ ██║██║ ╚████║
*/
#region QuoteItemLoan level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> LoanExistsAsync(long id)
{
return await ct.QuoteItemLoan.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemLoan> LoanCreateAsync(QuoteItemLoan newObject)
{
await LoanValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await LoanBizActionsAsync(AyaEvent.Created, newObject, null, null);
await ct.QuoteItemLoan.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await LoanSearchIndexAsync(newObject, true);
await LoanPopulateVizFields(newObject);
await LoanHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemLoan> LoanGetAsync(long id, bool logTheGetEvent = true)
{
if (UserIsSubContractorRestricted) //no access allowed at all
return null;
var ret = await ct.QuoteItemLoan.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<QuoteItemLoan> LoanPutAsync(QuoteItemLoan 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;
}
await LoanValidateAsync(putObject, dbObject);
if (HasErrors) return null;
await LoanBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await LoanExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, dbObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await LoanSearchIndexAsync(putObject, false);
await LoanPopulateVizFields(putObject);
await LoanHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> LoanDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await LoanGetAsync(id, false);
LoanValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.QuoteItemLoan.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await LoanHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task LoanSearchIndexAsync(QuoteItemLoan 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(QuoteItemLoan o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (loanUnitRateEnumList == null)
loanUnitRateEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList(
StringUtil.TrimTypeName(typeof(LoanUnitRateUnit).ToString()),
UserTranslationId,
CurrentUserRoles);
o.UnitOfMeasureViz = loanUnitRateEnumList.Where(x => x.Id == (long)o.Rate).Select(x => x.Name).First();
}
if (!vc.Has("loanunit", o.LoanUnitId))
vc.Add(await ct.LoanUnit.AsNoTracking().Where(x => x.Id == o.LoanUnitId).Select(x => x.Name).FirstOrDefaultAsync(), "loanunit", o.LoanUnitId);
o.LoanUnitViz = vc.Get("loanunit", o.LoanUnitId);
TaxCode Tax = null;
if (o.TaxCodeId != null)
{
if (!oc.Has("tax", o.TaxCodeId))
{
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId);
oc.Add(Tax, "tax", o.TaxCodeId);
}
else
Tax = (TaxCode)oc.Get("tax", o.TaxCodeId);
}
if (Tax != null)
o.TaxCodeViz = Tax.Name;
//manual price overrides anything
o.PriceViz = o.ListPrice;
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
//Currently not contract discounted so no further calcs need apply to priceViz
//Calculate totals and taxes
//NET
o.NetViz = MoneyUtil.Round(o.PriceViz * o.Quantity);
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
}
else
{
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
//RESTRICTED COST FIELD??
if (!UserCanViewLoanerCosts)
o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire
}
private List<NameIdItem> loanUnitRateEnumList = null;
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task LoanBizActionsAsync(AyaEvent ayaEvent, QuoteItemLoan newObj, QuoteItemLoan oldObj, IDbContextTransaction transaction)
{
//automatic actions on record change, called AFTER validation
//currently no processing required except for created or modified at this time
if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
return;
//SNAPSHOT PRICING
bool SnapshotPricing = true;
//if modifed, see what has changed and should be re-applied
if (ayaEvent == AyaEvent.Modified)
{
//If it wasn't a complete part change there is no need to set pricing
if (newObj.LoanUnitId == oldObj.LoanUnitId && newObj.Rate == oldObj.Rate)
{
SnapshotPricing = false;
//maintain old cost as it can come from the client as zero when it shouldn't be or someone using the api and setting it directly
//but we will only allow the price *we* set at the server initially
newObj.Cost = oldObj.Cost;
}
}
//Pricing
if (SnapshotPricing)
{
//default in case nothing to apply
newObj.Cost = 0;
newObj.ListPrice = 0;
LoanUnit loanUnit = await ct.LoanUnit.AsNoTracking().FirstOrDefaultAsync(x => x.Id == newObj.LoanUnitId);
if (loanUnit != null)
{
switch (newObj.Rate)
{
case LoanUnitRateUnit.None:
break;
case LoanUnitRateUnit.Hours:
newObj.Cost = loanUnit.RateHourCost;
newObj.ListPrice = loanUnit.RateHour;
break;
case LoanUnitRateUnit.HalfDays:
newObj.Cost = loanUnit.RateHalfDayCost;
newObj.ListPrice = loanUnit.RateHalfDay;
break;
case LoanUnitRateUnit.Days:
newObj.Cost = loanUnit.RateDayCost;
newObj.ListPrice = loanUnit.RateDay;
break;
case LoanUnitRateUnit.Weeks:
newObj.Cost = loanUnit.RateWeekCost;
newObj.ListPrice = loanUnit.RateWeek;
break;
case LoanUnitRateUnit.Months:
newObj.Cost = loanUnit.RateMonthCost;
newObj.ListPrice = loanUnit.RateMonth;
break;
case LoanUnitRateUnit.Years:
newObj.Cost = loanUnit.RateYearCost;
newObj.ListPrice = loanUnit.RateYear;
break;
}
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task LoanValidateAsync(QuoteItemLoan proposedObj, QuoteItemLoan currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (UserIsRestrictedType)
{
//no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (proposedObj.LoanUnitId < 1 || !await ct.LoanUnit.AnyAsync(x => x.Id == proposedObj.LoanUnitId))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "LoanUnitId");
if (proposedObj.Quantity < 0)//negative quantities are not allowed
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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
}
}
private void LoanValidateCanDelete(QuoteItemLoan obj)
{
if (UserIsRestrictedType)
{
//no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
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.QuoteItemLoan))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task LoanHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemLoan oProposed = (QuoteItemLoan)proposedObj;
var wid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
//for notification purposes because has no name / tags field itself
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item LOAN level
/*
██████╗ ██╗ ██╗████████╗███████╗██╗██████╗ ███████╗ ███████╗███████╗██████╗ ██╗ ██╗██╗ ██████╗███████╗
██╔═══██╗██║ ██║╚══██╔══╝██╔════╝██║██╔══██╗██╔════╝ ██╔════╝██╔════╝██╔══██╗██║ ██║██║██╔════╝██╔════╝
██║ ██║██║ ██║ ██║ ███████╗██║██║ ██║█████╗ ███████╗█████╗ ██████╔╝██║ ██║██║██║ █████╗
██║ ██║██║ ██║ ██║ ╚════██║██║██║ ██║██╔══╝ ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██║██║ ██╔══╝
╚██████╔╝╚██████╔╝ ██║ ███████║██║██████╔╝███████╗ ███████║███████╗██║ ██║ ╚████╔╝ ██║╚██████╗███████╗
╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝╚═════╝ ╚══════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚═╝ ╚═════╝╚══════╝
*/
#region QuoteItemOutsideService level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> OutsideServiceExistsAsync(long id)
{
return await ct.QuoteItemOutsideService.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemOutsideService> OutsideServiceCreateAsync(QuoteItemOutsideService newObject)
{
await OutsideServiceValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.QuoteItemOutsideService.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await OutsideServiceSearchIndexAsync(newObject, true);
await OutsideServicePopulateVizFields(newObject);
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemOutsideService> OutsideServiceGetAsync(long id, bool logTheGetEvent = true)
{
if (UserIsSubContractorRestricted || UserIsSubContractorFull) //no access allowed at all
return null;
var ret = await ct.QuoteItemOutsideService.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, AyaType.QuoteItemOutsideService, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<QuoteItemOutsideService> OutsideServicePutAsync(QuoteItemOutsideService putObject)
{
QuoteItemOutsideService 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;
}
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 OutsideServicePopulateVizFields(putObject);
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> OutsideServiceDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await OutsideServiceGetAsync(id, false);
OutsideServiceValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.QuoteItemOutsideService.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await OutsideServiceHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task OutsideServiceSearchIndexAsync(QuoteItemOutsideService 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(QuoteItemOutsideService o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.UnitId != 0)
{
if (!vc.Has("unitserial", o.UnitId))
vc.Add(await ct.Unit.AsNoTracking().Where(x => x.Id == o.UnitId).Select(x => x.Serial).FirstOrDefaultAsync(), "unitserial", o.UnitId);
o.UnitViz = vc.Get("unitserial", o.UnitId);
}
if (o.VendorSentToId != null)
{
if (!vc.Has("vendorname", o.VendorSentToId))
vc.Add(await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentToId).Select(x => x.Name).FirstOrDefaultAsync(), "vendorname", o.VendorSentToId);
o.VendorSentToViz = vc.Get("vendorname", o.VendorSentToId);
}
if (o.VendorSentViaId != null)
{
if (!vc.Has("vendorname", o.VendorSentViaId))
vc.Add(await ct.Vendor.AsNoTracking().Where(x => x.Id == o.VendorSentViaId).Select(x => x.Name).FirstOrDefaultAsync(), "vendorname", o.VendorSentViaId);
o.VendorSentViaViz = vc.Get("vendorname", o.VendorSentViaId);
}
}
TaxCode Tax = null;
if (o.TaxCodeId != null)
{
if (!oc.Has("tax", o.TaxCodeId))
{
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeId);
oc.Add(Tax, "tax", o.TaxCodeId);
}
else
Tax = (TaxCode)oc.Get("tax", o.TaxCodeId);
}
if (Tax != null)
o.TaxCodeViz = Tax.Name;
o.CostViz = o.ShippingCost + o.RepairCost;
o.PriceViz = o.ShippingPrice + o.RepairPrice;
//Currently not contract discounted so no further calcs need apply to priceViz
//Calculate totals and taxes
//NET
o.NetViz = o.PriceViz;//just for standardization, no quantity so is redundant but reporting easier if all the same
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
}
else
{
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task OutsideServiceValidateAsync(QuoteItemOutsideService proposedObj, QuoteItemOutsideService currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (UserIsRestrictedType)
{
//no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (proposedObj.UnitId < 1 || !await ct.Unit.AnyAsync(x => x.Id == proposedObj.UnitId))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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
}
}
private void OutsideServiceValidateCanDelete(QuoteItemOutsideService obj)
{
if (UserIsRestrictedType)
{
//no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
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.QuoteItemOutsideService))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task OutsideServiceHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
/*
OutsideServiceOverdue = 16,//* Workorder object , WorkOrderItemOutsideService created / updated, sets advance notice on due date tag filterable
OutsideServiceReceived = 17,//* Workorder object , WorkOrderItemOutsideService updated, instant notification when item received, tag filterable
*/
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemOutsideService oProposed = (QuoteItemOutsideService)proposedObj;
var wid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
//for notification purposes because has no name / tags field itself
oProposed.Name = WorkorderInfo.Serial.ToString();
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item OUTSIDE SERVICE level
/*
██████╗ █████╗ ██████╗ ████████╗███████╗
██╔══██╗██╔══██╗██╔══██╗╚══██╔══╝██╔════╝
██████╔╝███████║██████╔╝ ██║ ███████╗
██╔═══╝ ██╔══██║██╔══██╗ ██║ ╚════██║
██║ ██║ ██║██║ ██║ ██║ ███████║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚══════╝
*/
#region QuoteItemPart level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> PartExistsAsync(long id)
{
return await ct.QuoteItemPart.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemPart> CreatePartAsync(QuoteItemPart newObject)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
await PartValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await PartBizActionsAsync(AyaEvent.Created, newObject, null, null);
await ct.QuoteItemPart.AddAsync(newObject);
await ct.SaveChangesAsync();
if (HasErrors)
{
await transaction.RollbackAsync();
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await PartSearchIndexAsync(newObject, true);
await transaction.CommitAsync();
await PartPopulateVizFields(newObject);
await PartHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemPart> PartGetAsync(long id, bool logTheGetEvent = true)
{
if (UserIsSubContractorRestricted) //no access allowed at all
return null;
var ret = await ct.QuoteItemPart.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<QuoteItemPart> PartPutAsync(QuoteItemPart putObject)
{
using (var transaction = await ct.Database.BeginTransactionAsync())
{
QuoteItemPart 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;
}
await PartValidateAsync(putObject, dbObject);
if (HasErrors) return null;
await PartBizActionsAsync(AyaEvent.Modified, putObject, dbObject, null);
ct.Replace(dbObject, putObject);
try
{
await ct.SaveChangesAsync();
if (HasErrors)
{
await transaction.RollbackAsync();
return null;
}
}
catch (DbUpdateConcurrencyException)
{
if (!await PartExistsAsync(putObject.Id))
AddError(ApiErrorCode.NOT_FOUND);
else
AddError(ApiErrorCode.CONCURRENCY_CONFLICT);
return null;
}
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, putObject.Id, putObject.AyaType, AyaEvent.Modified), ct);
await PartSearchIndexAsync(putObject, false);
await transaction.CommitAsync();
await PartPopulateVizFields(putObject);
await PartHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> PartDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await PartGetAsync(id, false);
PartValidateCanDelete(dbObject);
if (HasErrors)
return false;
await PartBizActionsAsync(AyaEvent.Deleted, null, dbObject, transaction);
ct.QuoteItemPart.Remove(dbObject);
await ct.SaveChangesAsync();
if (HasErrors)
{
await transaction.RollbackAsync();
return false;
}
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await PartHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task PartSearchIndexAsync(QuoteItemPart 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(QuoteItemPart o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.PartWarehouseId != 0)
{
if (!vc.Has("partwarehouse", o.PartWarehouseId))
vc.Add(await ct.PartWarehouse.AsNoTracking().Where(x => x.Id == o.PartWarehouseId).Select(x => x.Name).FirstOrDefaultAsync(), "partwarehouse", o.PartWarehouseId);
o.PartWarehouseViz = vc.Get("partwarehouse", o.PartWarehouseId);
}
}
Part part = null;
if (o.PartId != 0)
{
if (!oc.Has("part", o.PartId))
{
part = await ct.Part.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.PartId);
oc.Add(part, "part", o.PartId);
}
else
part = (Part)oc.Get("part", o.PartId);
}
else
return;//this should never happen but this is insurance in case it does
o.PartDescriptionViz = part.Description;
o.PartNameViz = part.Name;
o.UpcViz = part.UPC;
TaxCode Tax = null;
if (o.TaxPartSaleId != null)
{
if (!oc.Has("tax", o.TaxPartSaleId))
{
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxPartSaleId);
oc.Add(Tax, "tax", o.TaxPartSaleId);
}
else
Tax = (TaxCode)oc.Get("tax", o.TaxPartSaleId);
}
if (Tax != null)
o.TaxCodeViz = Tax.Name;
o.PriceViz = 0;
if (part != null)
{
//COST & PRICE NOT SET HERE, SET IN BIZACTIONS SNAPSHOTTED
// o.CostViz = part.Cost;
// o.ListPriceViz = part.Retail;
o.UnitOfMeasureViz = part.UnitOfMeasure;
o.PriceViz = o.ListPrice;//default price used if not manual or contract override
}
//manual price overrides anything
if (o.PriceOverride != null)
o.PriceViz = (decimal)o.PriceOverride;
else
{
//not manual so could potentially have a contract adjustment
var c = await GetCurrentQuoteContractFromRelatedAsync(AyaType.QuoteItem, o.QuoteItemId);
if (c != null)
{
decimal pct = 0;
ContractOverrideType cot = ContractOverrideType.PriceDiscount;
bool TaggedAdjustmentInEffect = false;
//POTENTIAL CONTRACT ADJUSTMENTS
//First check if there is a matching tagged contract discount, that takes precedence
if (c.ContractPartOverrideItems.Count > 0)
{
//Iterate all contract tagged items in order of ones with the most tags first
foreach (var cp in c.ContractPartOverrideItems.OrderByDescending(z => z.Tags.Count))
if (cp.Tags.All(z => part.Tags.Any(x => x == z)))
{
if (cp.OverridePct != 0)
{
pct = cp.OverridePct / 100;
cot = cp.OverrideType;
TaggedAdjustmentInEffect = true;
}
}
}
//Generic discount?
if (!TaggedAdjustmentInEffect && c.ServiceRatesOverridePct != 0)
{
pct = c.ServiceRatesOverridePct / 100;
cot = c.ServiceRatesOverrideType;
}
//apply if discount found
if (pct != 0)
{
if (cot == ContractOverrideType.CostMarkup)
o.PriceViz = MoneyUtil.Round(o.Cost + (o.Cost * pct));
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = MoneyUtil.Round(o.ListPrice - (o.ListPrice * pct));
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = MoneyUtil.Round(o.PriceViz * o.Quantity);
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
}
else
{
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
//RESTRICTED COST FIELD??
if (!UserCanViewPartCosts)
o.Cost = 0;//cost already used in calcs and will not be updated on any update operation so this ensures the cost isn't sent over the wire
}
////////////////////////////////////////////////////////////////////////////////////////////////
//BIZ ACTIONS
//
//
private async Task PartBizActionsAsync(AyaEvent ayaEvent, QuoteItemPart newObj, QuoteItemPart oldObj, IDbContextTransaction transaction)
{
//SNAPSHOT PRICING IF NECESSARY
if (ayaEvent != AyaEvent.Created && ayaEvent != AyaEvent.Modified)
return;
//SNAPSHOT PRICING
bool SnapshotPricing = true;
//if modifed, see what has changed and should be re-applied
if (ayaEvent == AyaEvent.Modified)
{
//If it wasn't a complete part change there is no need to set pricing
if (newObj.PartId == oldObj.PartId)
{
SnapshotPricing = false;
//maintain old cost as it can come from the client as zero when it shouldn't be or someone using the api and setting it directly
//but we will only allow the price *we* set at the server initially
newObj.Cost = oldObj.Cost;
}
}
//Pricing
if (SnapshotPricing)
{
//default in case nothing to apply
newObj.Cost = 0;
newObj.ListPrice = 0;
var s = await ct.Part.AsNoTracking().FirstOrDefaultAsync(z => z.Id == newObj.PartId);
if (s != null)
{
newObj.Cost = s.Cost;
newObj.ListPrice = s.Retail;
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task PartValidateAsync(QuoteItemPart proposedObj, QuoteItemPart currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (UserIsRestrictedType)
{
//Parts: no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (!await BizObjectExistsInDatabase.ExistsAsync(AyaType.Part, proposedObj.PartId, ct))
{
AddError(ApiErrorCode.NOT_FOUND, "PartId");
return;
}
if (proposedObj.Quantity < 0)//negative quantities are not allowed
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "Quantity");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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
}
}
private void PartValidateCanDelete(QuoteItemPart obj)
{
if (UserIsRestrictedType)
{
//Parts: no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
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.QuoteItemPart))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task PartHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemPart oProposed = (QuoteItemPart)proposedObj;
var wid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name / tags field itself
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item PARTS level
/*
███████╗ ██████╗██╗ ██╗███████╗██████╗ ██╗ ██╗██╗ ███████╗██████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗
██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗██║ ██║██║ ██╔════╝██╔══██╗ ██║ ██║██╔════╝██╔════╝██╔══██╗██╔════╝
███████╗██║ ███████║█████╗ ██║ ██║██║ ██║██║ █████╗ ██║ ██║█████╗██║ ██║███████╗█████╗ ██████╔╝███████╗
╚════██║██║ ██╔══██║██╔══╝ ██║ ██║██║ ██║██║ ██╔══╝ ██║ ██║╚════╝██║ ██║╚════██║██╔══╝ ██╔══██╗╚════██║
███████║╚██████╗██║ ██║███████╗██████╔╝╚██████╔╝███████╗███████╗██████╔╝ ╚██████╔╝███████║███████╗██║ ██║███████║
╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═════╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝
*/
#region QuoteItemScheduledUser level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> ScheduledUserExistsAsync(long id)
{
return await ct.QuoteItemScheduledUser.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemScheduledUser> ScheduledUserCreateAsync(QuoteItemScheduledUser newObject)
{
await ScheduledUserValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.QuoteItemScheduledUser.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await ScheduledUserPopulateVizFields(newObject);
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemScheduledUser> ScheduledUserGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.QuoteItemScheduledUser.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (UserIsRestrictedType && ret.UserId != UserId)//restricted users can only see their own
return null;
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<QuoteItemScheduledUser> ScheduledUserPutAsync(QuoteItemScheduledUser putObject)
{
QuoteItemScheduledUser 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;
}
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 ScheduledUserPopulateVizFields(putObject);
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> ScheduledUserDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await ScheduledUserGetAsync(id, false);
ScheduledUserValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.QuoteItemScheduledUser.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await ScheduledUserHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VIZ POPULATE
//
private async Task ScheduledUserPopulateVizFields(QuoteItemScheduledUser o)
{
if (o.UserId != null)
{
if (!vc.Has("user", o.UserId))
{
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
}
o.UserViz = vc.Get("user", o.UserId);
}
if (o.ServiceRateId != null)
{
if (!vc.Has("servicerate", o.ServiceRateId))
{
vc.Add(await ct.ServiceRate.AsNoTracking().Where(x => x.Id == o.ServiceRateId).Select(x => x.Name).FirstOrDefaultAsync(), "servicerate", o.ServiceRateId);
}
o.ServiceRateViz = vc.Get("servicerate", o.ServiceRateId);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task ScheduledUserValidateAsync(QuoteItemScheduledUser proposedObj, QuoteItemScheduledUser currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
if (UserIsRestrictedType)
{
//Scheduled Users: view only where they are the selected User and convert to labor record
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
//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 GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (proposedObj.EstimatedQuantity < 0)//negative quantities are not allowed
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "EstimatedQuantity");
//Start date AND end date must both be null or both contain values
if (proposedObj.StartDate == null && proposedObj.StopDate != null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "StopDate");
if (proposedObj.StartDate != null && proposedObj.StopDate == null)
AddError(ApiErrorCode.VALIDATION_REQUIRED, "StopDate");
//Start date before end date
if (proposedObj.StartDate != null && proposedObj.StopDate != null)
if (proposedObj.StartDate > proposedObj.StopDate)
AddError(ApiErrorCode.VALIDATION_STARTDATE_AFTER_ENDDATE, "StartDate");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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
}
}
private void ScheduledUserValidateCanDelete(QuoteItemScheduledUser obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
if (UserIsRestrictedType)
{
//Scheduled Users: view only where they are the selected User and convert to labor record
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.QuoteItemScheduledUser))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task ScheduledUserHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemScheduledUser oProposed = (QuoteItemScheduledUser)proposedObj;
var wid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name field itself
oProposed.Tags = WorkorderInfo.Tags; //for notification purposes because has no tag field itself
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item SCHEDULED USER level
/*
████████╗ █████╗ ███████╗██╗ ██╗
╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝
██║ ███████║███████╗█████╔╝
██║ ██╔══██║╚════██║██╔═██╗
██║ ██║ ██║███████║██║ ██╗
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝
*/
#region QuoteItemTask level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> TaskExistsAsync(long id)
{
return await ct.QuoteItemTask.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemTask> TaskCreateAsync(QuoteItemTask newObject)
{
await TaskValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.QuoteItemTask.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await TaskSearchIndexAsync(newObject, true);
await TaskPopulateVizFields(newObject);
await TaskHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemTask> TaskGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.QuoteItemTask.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<QuoteItemTask> TaskPutAsync(QuoteItemTask putObject)
{
QuoteItemTask 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;
}
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 TaskPopulateVizFields(putObject);
await TaskHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> TaskDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await TaskGetAsync(id, false);
TaskValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.QuoteItemTask.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await TaskHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task TaskSearchIndexAsync(QuoteItemTask 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(QuoteItemTask o)
{
if (o.CompletedByUserId != null)
{
if (!vc.Has("user", o.CompletedByUserId))
{
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.CompletedByUserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.CompletedByUserId);
}
o.CompletedByUserViz = vc.Get("user", o.CompletedByUserId);
}
if (taskCompletionTypeEnumList == null)
taskCompletionTypeEnumList = await AyaNova.Api.Controllers.EnumListController.GetEnumList(
StringUtil.TrimTypeName(typeof(WorkOrderItemTaskCompletionType).ToString()),
UserTranslationId,
CurrentUserRoles);
o.StatusViz = taskCompletionTypeEnumList.Where(x => x.Id == (long)o.Status).Select(x => x.Name).First();
}
private List<NameIdItem> taskCompletionTypeEnumList = null;
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task TaskValidateAsync(QuoteItemTask proposedObj, QuoteItemTask currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (isNew && UserIsRestrictedType)
{
//restricted users are not allowed to make new task entries only fill them out
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
if (!isNew && UserIsRestrictedType)
{
//Existing record so just make sure they haven't changed the not changeable fields from the db version
//* Tasks: view and edit existing tasks, set completion type and date only, no add or remove or changing other fields
//note that UI will prevent this, this rule is only backup for 3rd party api users
if (currentObj.Task != proposedObj.Task) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Task");
if (currentObj.CompletedByUserId != UserId) AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "CompletedByUserId");
if (currentObj.Sequence != proposedObj.Sequence) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Sequence");
//they *are* permitted to change the date and status but only if they are the selected user:
//note that UI will prevent this, this rule is only backup for 3rd party api users
if (currentObj.CompletedDate != proposedObj.CompletedDate && currentObj.CompletedByUserId != UserId) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "CompletedDate");
if (currentObj.Status != proposedObj.Status && currentObj.CompletedByUserId != UserId) AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "Status");
}
if (string.IsNullOrWhiteSpace(proposedObj.Task))
AddError(ApiErrorCode.VALIDATION_REQUIRED, "Task");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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
}
}
private void TaskValidateCanDelete(QuoteItemTask obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
if (UserIsRestrictedType)
{
//restricted users are not allowed to delete a task only fill them out
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.QuoteItemTask))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task TaskHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemTask oProposed = (QuoteItemTask)proposedObj;
var wid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString(); //for notification purposes because has no name / tags field itself
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item TASK level
/*
████████╗██████╗ █████╗ ██╗ ██╗███████╗██╗
╚══██╔══╝██╔══██╗██╔══██╗██║ ██║██╔════╝██║
██║ ██████╔╝███████║██║ ██║█████╗ ██║
██║ ██╔══██╗██╔══██║╚██╗ ██╔╝██╔══╝ ██║
██║ ██║ ██║██║ ██║ ╚████╔╝ ███████╗███████╗
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝
*/
#region QuoteItemTravel level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> TravelExistsAsync(long id)
{
return await ct.QuoteItemTravel.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemTravel> TravelCreateAsync(QuoteItemTravel newObject)
{
await TravelValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
await ct.QuoteItemTravel.AddAsync(newObject);
await ct.SaveChangesAsync();
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, newObject.Id, newObject.AyaType, AyaEvent.Created), ct);
await TravelSearchIndexAsync(newObject, true);
await TravelPopulateVizFields(newObject);
await TravelHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemTravel> TravelGetAsync(long id, bool logTheGetEvent = true)
{
var ret = await ct.QuoteItemTravel.AsNoTracking().SingleOrDefaultAsync(z => z.Id == id);
if (UserIsRestrictedType && ret.UserId != UserId)
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return null;
}
if (logTheGetEvent && ret != null)
await EventLogProcessor.LogEventToDatabaseAsync(new Event(UserId, id, ret.AyaType, AyaEvent.Retrieved), ct);
return ret;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//UPDATE
//
internal async Task<QuoteItemTravel> TravelPutAsync(QuoteItemTravel putObject)
{
QuoteItemTravel 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;
}
await TravelValidateAsync(putObject, dbObject);
if (HasErrors) return 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 TravelPopulateVizFields(putObject);
await TravelHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> TravelDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await TravelGetAsync(id, false);
TravelValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.QuoteItemTravel.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await TravelHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task TravelSearchIndexAsync(QuoteItemTravel 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(QuoteItemTravel o, bool calculateTotalsOnly = false)
{
if (calculateTotalsOnly == false)
{
if (o.UserId != null)
{
if (!vc.Has("user", o.UserId))
{
vc.Add(await ct.User.AsNoTracking().Where(x => x.Id == o.UserId).Select(x => x.Name).FirstOrDefaultAsync(), "user", o.UserId);
}
o.UserViz = vc.Get("user", o.UserId);
}
}
TravelRate Rate = null;
if (o.TravelRateId != null)
{
if (!oc.Has("travelrate", o.TravelRateId))
{
Rate = await ct.TravelRate.AsNoTracking().FirstOrDefaultAsync(x => x.Id == o.TravelRateId);
oc.Add(Rate, "travelrate", o.TravelRateId);
}
else
Rate = (TravelRate)oc.Get("travelrate", o.TravelRateId);
o.TravelRateViz = Rate.Name;
}
TaxCode Tax = null;
if (o.TaxCodeSaleId != null)
{
if (!oc.Has("tax", o.TaxCodeSaleId))
{
Tax = await ct.TaxCode.AsNoTracking().FirstOrDefaultAsync(z => z.Id == o.TaxCodeSaleId);
oc.Add(Tax, "tax", o.TaxCodeSaleId);
}
else
Tax = (TaxCode)oc.Get("tax", o.TaxCodeSaleId);
}
if (Tax != null)
o.TaxCodeViz = 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 GetCurrentQuoteContractFromRelatedAsync(AyaType.QuoteItem, o.QuoteItemId);
if (c != null)
{
decimal pct = 0;
ContractOverrideType cot = ContractOverrideType.PriceDiscount;
bool TaggedAdjustmentInEffect = false;
//POTENTIAL CONTRACT ADJUSTMENTS
//First check if there is a matching tagged Travel rate contract discount, that takes precedence
if (c.ContractTravelRateOverrideItems.Count > 0 && Rate != null)
{
//Iterate all contract tagged items in order of ones with the most tags first
foreach (var csr in c.ContractTravelRateOverrideItems.OrderByDescending(z => z.Tags.Count))
if (csr.Tags.All(z => Rate.Tags.Any(x => x == z)))
{
if (csr.OverridePct != 0)
{
pct = csr.OverridePct / 100;
cot = csr.OverrideType;
TaggedAdjustmentInEffect = true;
}
}
}
//Generic discount?
if (!TaggedAdjustmentInEffect && c.TravelRatesOverridePct != 0)
{
pct = c.TravelRatesOverridePct / 100;
cot = c.TravelRatesOverrideType;
}
//apply if discount found
if (pct != 0)
{
if (cot == ContractOverrideType.CostMarkup)
o.PriceViz = MoneyUtil.Round(o.CostViz + (o.CostViz * pct));
else if (cot == ContractOverrideType.PriceDiscount)
o.PriceViz = MoneyUtil.Round(o.ListPriceViz - (o.ListPriceViz * pct));
}
}
}
//Calculate totals and taxes
//NET
o.NetViz = MoneyUtil.Round(o.PriceViz * o.TravelRateQuantity);
//TAX
o.TaxAViz = 0;
o.TaxBViz = 0;
if (Tax != null)
{
if (Tax.TaxAPct != 0)
{
o.TaxAViz = MoneyUtil.Round(o.NetViz * (Tax.TaxAPct / 100));
}
if (Tax.TaxBPct != 0)
{
if (Tax.TaxOnTax)
{
o.TaxBViz = MoneyUtil.Round((o.NetViz + o.TaxAViz) * (Tax.TaxBPct / 100));
}
else
{
o.TaxBViz = MoneyUtil.Round(o.NetViz * (Tax.TaxBPct / 100));
}
}
}
o.LineTotalViz = o.NetViz + o.TaxAViz + o.TaxBViz;
//RESTRICTIONS ON COST VISIBILITY?
if (!UserCanViewLaborOrTravelRateCosts)
{
o.CostViz = 0;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task TravelValidateAsync(QuoteItemTravel proposedObj, QuoteItemTravel currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (UserIsRestrictedType && (proposedObj.UserId != UserId || (!isNew && currentObj.UserId != UserId)))
{
//no edits allowed on other people's records
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
if (proposedObj.TravelRateQuantity < 0)//negative quantities are not allowed
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "TravelRateQuantity");
if (proposedObj.NoChargeQuantity < 0)//negative quantities are not allowed
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "NoChargeQuantity");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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
}
}
private void TravelValidateCanDelete(QuoteItemTravel obj)
{
if (obj == null)
{
AddError(ApiErrorCode.NOT_FOUND, "id");
return;
}
if (UserIsRestrictedType)
{
//Travels: add (no user selection defaults to themselves), remove, view and edit only when they are the selected User
if (obj.UserId != UserId)
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
//re-check rights here necessary due to traversal delete from Principle object
if (!Authorized.HasDeleteRole(CurrentUserRoles, AyaType.QuoteItemTravel))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task TravelHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemTravel oProposed = (QuoteItemTravel)proposedObj;
var wid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();//for notification purposes because has no name / tags field itself
oProposed.Tags = WorkorderInfo.Tags;
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item TRAVEL level
/*
██╗ ██╗███╗ ██╗██╗████████╗
██║ ██║████╗ ██║██║╚══██╔══╝
██║ ██║██╔██╗ ██║██║ ██║
██║ ██║██║╚██╗██║██║ ██║
╚██████╔╝██║ ╚████║██║ ██║
╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝
*/
#region QuoteItemUnit level
////////////////////////////////////////////////////////////////////////////////////////////////
//EXISTS
internal async Task<bool> UnitExistsAsync(long id)
{
return await ct.QuoteItemUnit.AnyAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//CREATE
//
internal async Task<QuoteItemUnit> UnitCreateAsync(QuoteItemUnit newObject)
{
await UnitValidateAsync(newObject, null);
if (HasErrors)
return null;
else
{
newObject.Tags = TagBiz.NormalizeTags(newObject.Tags);
newObject.CustomFields = JsonUtil.CompactJson(newObject.CustomFields);
await ct.QuoteItemUnit.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 UnitPopulateVizFields(newObject, false);
await UnitHandlePotentialNotificationEvent(AyaEvent.Created, newObject);
return newObject;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// GET
//
internal async Task<QuoteItemUnit> UnitGetAsync(long id, bool logTheGetEvent = true)
{
if (UserIsSubContractorRestricted) //no access allowed at all
return null;
var ret = await ct.QuoteItemUnit.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<QuoteItemUnit> UnitPutAsync(QuoteItemUnit putObject)
{
QuoteItemUnit 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;
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 UnitPopulateVizFields(putObject, false);
await UnitHandlePotentialNotificationEvent(AyaEvent.Modified, putObject, dbObject);
return putObject;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//DELETE
//
internal async Task<bool> UnitDeleteAsync(long id, IDbContextTransaction parentTransaction = null)
{
var transaction = parentTransaction ?? await ct.Database.BeginTransactionAsync();
var dbObject = await UnitGetAsync(id, false);
UnitValidateCanDelete(dbObject);
if (HasErrors)
return false;
ct.QuoteItemUnit.Remove(dbObject);
await ct.SaveChangesAsync();
//Log event
await EventLogProcessor.DeleteObjectLogAsync(UserId, dbObject.AyaType, dbObject.Id, "quoteitem:" + dbObject.QuoteItemId.ToString(), ct);//Fix??
await Search.ProcessDeletedObjectKeywordsAsync(dbObject.Id, dbObject.AyaType, ct);
if (parentTransaction == null)
await transaction.CommitAsync();
await UnitHandlePotentialNotificationEvent(AyaEvent.Deleted, dbObject);
return true;
}
//////////////////////////////////////////////
//INDEXING
//
private async Task UnitSearchIndexAsync(QuoteItemUnit obj, bool isNew)
{
//SEARCH INDEXING
var SearchParams = new Search.SearchIndexProcessObjectParameters(UserTranslationId, obj.Id, obj.AyaType);
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> UnitGetSearchResultSummary(long id)
{
var obj = await UnitGetAsync(id, false);
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 UnitPopulateVizFields(QuoteItemUnit o, bool populateForReporting)
{
//see if it's in the cache already, populate the cache fully if not
bool UnitHasModel = false;
if (!vc.Has("unitserial", o.UnitId))
{
//cache it
var unitInfo = await ct.Unit.AsNoTracking()
.Where(x => x.Id == o.UnitId)
.Select(x => new { x.Serial, x.Description, x.UnitModelId, x.Address, x.City, x.Region, x.Country, x.Latitude, x.Longitude, x.Metered })
.FirstOrDefaultAsync();
vc.Add(unitInfo.Serial, "unitserial", o.UnitId);
vc.Add(unitInfo.Description, "unitdesc", o.UnitId);
vc.Add(unitInfo.Address, "unitaddr", o.UnitId);
vc.Add(unitInfo.City, "unitcity", o.UnitId);
vc.Add(unitInfo.Region, "unitregion", o.UnitId);
vc.Add(unitInfo.Country, "unitcountry", o.UnitId);
vc.Add(unitInfo.Latitude.ToString(), "unitlat", o.UnitId);
vc.Add(unitInfo.Longitude.ToString(), "unitlong", o.UnitId);
vc.Add(unitInfo.Metered.ToString(), "unitmetered", o.UnitId);
if (unitInfo.UnitModelId != null)
{
UnitHasModel = true;
//units model name cached? (if it is then the rest will be cached as well)
if (!vc.Has("unitsmodelname", o.UnitId))
{
//nope, model name cached??
if (!vc.Has("unitmodelname", unitInfo.UnitModelId))
{
//nope, so cache it all
var unitModelInfo = await ct.UnitModel.AsNoTracking().Where(x => x.Id == unitInfo.UnitModelId).Select(x => new { x.Name, x.VendorId }).FirstOrDefaultAsync();
vc.Add(unitModelInfo.Name, "unitmodelname", unitInfo.UnitModelId);
vc.Add(unitModelInfo.Name, "unitsmodelname", o.UnitId);
if (unitModelInfo.VendorId != null)
{
var ModelVendorName = vc.Get("unitsmodelvendorname", o.UnitId);
if (ModelVendorName == null)
{
ModelVendorName = vc.Get("vendorname", unitModelInfo.VendorId);
if (ModelVendorName == null)
{
ModelVendorName = await ct.Vendor.AsNoTracking().Where(x => x.Id == unitModelInfo.VendorId).Select(x => x.Name).FirstOrDefaultAsync();
vc.Add(ModelVendorName, "vendorname", unitModelInfo.VendorId);
vc.Add(ModelVendorName, "unitsmodelvendorname", o.UnitId);
}
else
{
//cached under vendor so reuse here
vc.Add(ModelVendorName, "unitsmodelvendorname", o.UnitId);
}
}
}
}
}
}
}
//populate all fields from cache
if (UnitHasModel)
{
o.UnitModelNameViz = vc.Get("unitsmodelname", o.UnitId);
o.UnitModelVendorViz = vc.Get("unitsmodelvendorname", o.UnitId);
}
o.UnitViz = vc.Get("unitserial", o.UnitId);
o.UnitDescriptionViz = vc.Get("unitdesc", o.UnitId);
// o.UnitMeteredViz = vc.GetAsBool("unitmetered", o.UnitId);
if (populateForReporting)
{
o.AddressViz = vc.Get("unitaddr", o.UnitId);
o.CityViz = vc.Get("unitcity", o.UnitId);
o.RegionViz = vc.Get("unitregion", o.UnitId);
o.CountryViz = vc.Get("unitcountry", o.UnitId);
o.LatitudeViz = vc.GetAsDecimal("unitlat", o.UnitId);
o.LongitudeViz = vc.GetAsDecimal("unitlong", o.UnitId);
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//VALIDATION
//
private async Task UnitValidateAsync(QuoteItemUnit proposedObj, QuoteItemUnit currentObj)
{
//skip validation if seeding
// if(ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
//run validation and biz rules
bool isNew = currentObj == null;
if (UserIsRestrictedType)
{
//Units: no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
if (proposedObj.QuoteItemId == 0)
{
AddError(ApiErrorCode.VALIDATION_REQUIRED, "QuoteItemId");
return;//this is a completely disqualifying error
}
else if (!await ItemExistsAsync(proposedObj.QuoteItemId))
{
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "QuoteItemId");
return;//this is a completely disqualifying error
}
//Check state if updatable right now
if (!isNew)
{
//Front end is coded to save the state first before any other updates if it has changed and it would not be
//a part of this header update so it's safe to check it here as it will be most up to date
var CurrentWoStatus = await GetCurrentQuoteStatusFromRelatedAsync(proposedObj.AyaType, proposedObj.Id);
if (CurrentWoStatus.Locked)
{
AddError(ApiErrorCode.VALIDATION_NOT_CHANGEABLE, "generalerror", await Translate("QuoteErrorLocked"));
return;//this is a completely disqualifying error
}
}
if (proposedObj.UnitId < 1 || !await ct.Unit.AnyAsync(x => x.Id == proposedObj.UnitId))
AddError(ApiErrorCode.VALIDATION_INVALID_VALUE, "UnitId");
//Any form customizations to validate?
var FormCustomization = await ct.FormCustom.AsNoTracking().SingleOrDefaultAsync(z => z.FormKey == AyaType.Quote.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(QuoteItemUnit obj)
{
if (UserIsRestrictedType)
{
//Units: no edits allowed
AddError(ApiErrorCode.NOT_AUTHORIZED, "generalerror");
return;
}
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.QuoteItemUnit))
{
AddError(ApiErrorCode.NOT_AUTHORIZED);
return;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
// NOTIFICATION PROCESSING
//
public async Task UnitHandlePotentialNotificationEvent(AyaEvent ayaEvent, ICoreBizObjectModel proposedObj, ICoreBizObjectModel currentObj = null)
{
ILogger log = AyaNova.Util.ApplicationLogging.CreateLogger<QuoteBiz>();
if (ServerBootConfig.SEEDING || ServerBootConfig.MIGRATING) return;
log.LogDebug($"HandlePotentialNotificationEvent processing: [AyaType:{proposedObj.AyaType}, AyaEvent:{ayaEvent}]");
bool isNew = currentObj == null;
QuoteItemUnit oProposed = (QuoteItemUnit)proposedObj;
var wid = await GetQuoteIdFromRelativeAsync(AyaType.QuoteItem, oProposed.QuoteItemId, ct);
var WorkorderInfo = await ct.Quote.AsNoTracking().Where(x => x.Id == wid.ParentId).Select(x => new { Serial = x.Serial, Tags = x.Tags }).FirstOrDefaultAsync();
oProposed.Name = WorkorderInfo.Serial.ToString();//for notification purposes because has no name field itself
//STANDARD EVENTS FOR ALL OBJECTS
await NotifyEventHelper.ProcessStandardObjectEvents(ayaEvent, proposedObj, ct);
//SPECIFIC EVENTS FOR THIS OBJECT
}//end of process notifications
#endregion work order item LABOR level
#region Utility
public async Task<ICoreBizObjectModel> GetQuoteGraphItem(AyaType ayaType, long id)
{
switch (ayaType)
{
case AyaType.Quote:
return await QuoteGetAsync(id, false) as ICoreBizObjectModel;
case AyaType.QuoteItem:
return await ItemGetAsync(id, false);
case AyaType.QuoteItemExpense:
return await ExpenseGetAsync(id, false);
case AyaType.QuoteItemLabor:
return await LaborGetAsync(id, false);
case AyaType.QuoteItemLoan:
return await LoanGetAsync(id, false);
case AyaType.QuoteItemPart:
return await PartGetAsync(id, false);
case AyaType.QuoteItemScheduledUser:
return await ScheduledUserGetAsync(id, false);
case AyaType.QuoteItemTask:
return await TaskGetAsync(id, false);
case AyaType.QuoteItemTravel:
return await TravelGetAsync(id, false);
case AyaType.QuoteItemUnit:
return await UnitGetAsync(id, false);
case AyaType.QuoteItemOutsideService:
return await OutsideServiceGetAsync(id, false);
default:
throw new System.ArgumentOutOfRangeException($"Quote::GetQuoteGraphItem -> Invalid ayaType{ayaType}");
}
}
public async Task<ICoreBizObjectModel> PutQuoteGraphItem(AyaType ayaType, ICoreBizObjectModel o)
{
ClearErrors();
switch (ayaType)
{
case AyaType.Quote:
if (o is Quote)
{
Quote dto = new Quote();
CopyObject.Copy(o, dto, "Name");
return await QuotePutAsync((Quote)dto);
}
return await QuotePutAsync((Quote)o) as ICoreBizObjectModel;
case AyaType.QuoteItem:
if (o is QuoteItem)
{
QuoteItem dto = new QuoteItem();
CopyObject.Copy(o, dto);
return await ItemPutAsync((QuoteItem)dto);
}
return await ItemPutAsync((QuoteItem)o);
case AyaType.QuoteItemExpense:
return await ExpensePutAsync((QuoteItemExpense)o);
case AyaType.QuoteItemLabor:
return await LaborPutAsync((QuoteItemLabor)o);
case AyaType.QuoteItemLoan:
return await LoanPutAsync((QuoteItemLoan)o);
case AyaType.QuoteItemPart:
return await PartPutAsync((QuoteItemPart)o);
case AyaType.QuoteItemScheduledUser:
return await ScheduledUserPutAsync((QuoteItemScheduledUser)o);
case AyaType.QuoteItemTask:
return await TaskPutAsync((QuoteItemTask)o);
case AyaType.QuoteItemTravel:
return await TravelPutAsync((QuoteItemTravel)o);
case AyaType.QuoteItemUnit:
return await UnitPutAsync((QuoteItemUnit)o);
case AyaType.QuoteItemOutsideService:
return await OutsideServicePutAsync((QuoteItemOutsideService)o);
default:
throw new System.ArgumentOutOfRangeException($"Quote::PutQuoteGraphItem -> Invalid ayaType{ayaType}");
}
}
public async Task<bool> DeleteQuoteGraphItem(AyaType ayaType, long id)
{
switch (ayaType)
{
case AyaType.Quote:
return await QuoteDeleteAsync(id);
case AyaType.QuoteItem:
return await ItemDeleteAsync(id);
case AyaType.QuoteItemExpense:
return await ExpenseDeleteAsync(id);
case AyaType.QuoteItemLabor:
return await LaborDeleteAsync(id);
case AyaType.QuoteItemLoan:
return await LoanDeleteAsync(id);
case AyaType.QuoteItemPart:
return await PartDeleteAsync(id);
case AyaType.QuoteItemScheduledUser:
return await ScheduledUserDeleteAsync(id);
case AyaType.QuoteItemTask:
return await TaskDeleteAsync(id);
case AyaType.QuoteItemTravel:
return await TravelDeleteAsync(id);
case AyaType.QuoteItemUnit:
return await UnitDeleteAsync(id);
case AyaType.QuoteItemOutsideService:
return await OutsideServiceDeleteAsync(id);
default:
throw new System.ArgumentOutOfRangeException($"Quote::GetQuoteGraphItem -> 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> GetCurrentQuoteContractFromRelatedAsync(AyaType ayaType, long id)
{
if (mFetchedContractAlready == false)
{
var wid = await GetQuoteIdFromRelativeAsync(ayaType, id, ct);
var WoContractId = await ct.Quote.AsNoTracking().Where(z => z.Id == wid.ParentId).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 GetFullyPopulatedContractGraphFromIdAsync(id);
}
return mContractInEffect;
}
internal async Task<Contract> GetFullyPopulatedContractGraphFromIdAsync(long? id)
{
if (id == null) return null;
return await ct.Contract.AsSplitQuery().AsNoTracking()
.Include(c => c.ServiceRateItems)
.Include(c => c.TravelRateItems)
.Include(c => c.ContractPartOverrideItems)
.Include(c => c.ContractTravelRateOverrideItems)
.Include(c => c.ContractServiceRateOverrideItems)
.FirstOrDefaultAsync(z => z.Id == id);
}
////////////////////////////////////////////////////////////////////////////////////////////////
//GET CURRENT STATUS FOR WORKORDER FROM RELATIVE
//
//cache the state to save repeatedly fetching it for this operation which could be called multiple times in a flowv
internal QuoteStatus mCurrentQuoteStatus = null;
internal async Task<QuoteStatus> GetCurrentQuoteStatusFromRelatedAsync(AyaType ayaType, long id)
{
if (mCurrentQuoteStatus == null)
{
var wid = await GetQuoteIdFromRelativeAsync(ayaType, id, ct);
var stat = await ct.QuoteState.AsNoTracking()
.Where(z => z.QuoteId == wid.ParentId)
.OrderByDescending(z => z.Created)
.Take(1)
.FirstOrDefaultAsync();
//no state set yet?
if (stat == null)
mCurrentQuoteStatus = new QuoteStatus() { Id = -1, Locked = false, Completed = false };
else
mCurrentQuoteStatus = await ct.QuoteStatus.AsNoTracking().Where(z => z.Id == stat.QuoteStatusId).FirstAsync();//this should never not be null
}
return mCurrentQuoteStatus;
}
#endregion utility
/////////////////////////////////////////////////////////////////////
}//eoc
}//eons