public async Task <TechnicalFee> GetTechnicalFee(Guid costId)
        {
            var cost = await _efContext.Cost.FirstAsync(x => x.Id == costId);

            var stageDetailsForm = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(cost.LatestCostStageRevisionId.Value);

            //var productionDetailsForm = await _costStageRevisionService.GetProductionDetails<PgProductionDetailsForm>(cost.LatestCostStageRevisionId.Value);
            var budgetRegion   = stageDetailsForm.BudgetRegion?.Key;
            var contentType    = stageDetailsForm.ContentType?.Key;
            var costType       = cost.CostType;
            var productionType = Constants.ProductionType.ProductionTypeList.FirstOrDefault(a => a == stageDetailsForm.ProductionType?.Key);

            var queriable = _efContext.TechnicalFee.Where(x =>
                                                          x.CostType == cost.CostType.ToString() &&
                                                          x.RegionName == budgetRegion);

            // content type is not applicable to Usage&Buyout and Trafficking/Distribution
            if (costType != CostType.Buyout && costType != CostType.Trafficking)
            {
                queriable = queriable.Where(x => x.ContentType == contentType);
            }

            if (costType != CostType.Buyout && costType != CostType.Trafficking && contentType != Constants.ContentType.Digital)
            {
                queriable = queriable.Where(x => x.ProductionType == productionType);
            }

            return(await queriable.FirstOrDefaultAsync());
        }
        public bool DoesUserHaveRoleForCost(CostUser costUser, CostStageRevision costStageRevision, string businessRole)
        {
            if (costUser == null)
            {
                throw new ArgumentNullException(nameof(costUser));
            }

            if (costUser.UserBusinessRoles == null)
            {
                throw new ArgumentException($"{nameof(costUser.UserBusinessRoles)} are missing");
            }

            if (costStageRevision == null)
            {
                throw new ArgumentException($"{nameof(costStageRevision)} is missing");
            }

            if (costStageRevision.StageDetails == null)
            {
                throw new ArgumentException($"{nameof(costStageRevision.StageDetails)} is missing");
            }

            var stageDetails = _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevision);
            var smoName      = stageDetails.SmoName;

            var hasRole = costUser.UserBusinessRoles.Any(ubr =>
                                                         ubr.BusinessRole != null &&
                                                         ubr.BusinessRole.Key == businessRole &&
                                                         (ubr.ObjectId != null || ubr.Labels.Contains(smoName))
                                                         );

            return(hasRole);
        }
示例#3
0
        public async Task <ICollection <CostLineItem> > InterpolateCostLineItems(
            Guid costId,
            Guid costStageRevisionId,
            BillingExpenseData billingExpense)
        {
            /*
             *  Base Compensation Total feeds into "Base Compensation" line of Cost Section
             *  Pension & Health Total feeds into "Pension & Health" line of Cost Section
             *  Bonus Total feeds into “Bonus" line of Cost Section
             *  Agency Fee Total feeds into "Negotiation/broker agency fee
             *  Other incurred Costs Total feeds into "Other services and fees"
             */
            var interpolatedItems = new List <CostLineItem>();

            var stageForm = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevisionId);

            string contentType = stageForm.GetContentType();
            string production  = stageForm.GetProductionType();

            var cost = await _efContext.Cost
                       .Include(c => c.CostTemplateVersion)
                       .ThenInclude(ctv => ctv.CostTemplate)
                       .ThenInclude(ct => ct.FieldDefinitions)
                       .FirstOrDefaultAsync(c => c.Id == costId);

            var costStageRevision = await _efContext.CostStageRevision
                                    .Include(c => c.CostLineItems)
                                    .FirstOrDefaultAsync(c => c.Id == costStageRevisionId);

            var paymentCurrency = await _efContext.Currency.FirstOrDefaultAsync(c => c.Code == stageForm.AgencyCurrency);

            var templateModel = await _costTemplateVersionService.GetCostTemplateVersionModel(cost.CostTemplateVersionId);

            var form          = _costSectionFinder.GetCostSection(templateModel, contentType, production).Result;
            var costLineItems = costStageRevision.CostLineItems;

            var baseCompensation           = GetOrCreateCostLineItem(form, costLineItems, "baseCompensation", paymentCurrency);
            var pensionAndHealth           = GetOrCreateCostLineItem(form, costLineItems, "pensionAndHealth", paymentCurrency);
            var bonusCelebrityOnly         = GetOrCreateCostLineItem(form, costLineItems, "bonusCelebrityOnly", paymentCurrency);
            var negotiationBrokerAgencyFee = GetOrCreateCostLineItem(form, costLineItems, "negotiationBrokerAgencyFee", paymentCurrency);
            var otherServicesAndFees       = GetOrCreateCostLineItem(form, costLineItems, "otherServicesAndFees", paymentCurrency);

            interpolatedItems.AddRange(new[] { baseCompensation, pensionAndHealth, bonusCelebrityOnly, negotiationBrokerAgencyFee, otherServicesAndFees });

            baseCompensation.ValueInLocalCurrency           = GetBillingExpenseItemValue(billingExpense, Constants.BillingExpenseItem.UsageBuyoutFee);
            pensionAndHealth.ValueInLocalCurrency           = GetBillingExpenseItemValue(billingExpense, Constants.BillingExpenseItem.PensionAndHealth);
            bonusCelebrityOnly.ValueInLocalCurrency         = GetBillingExpenseItemValue(billingExpense, Constants.BillingExpenseItem.Bonus);
            negotiationBrokerAgencyFee.ValueInLocalCurrency = GetBillingExpenseItemValue(billingExpense, Constants.BillingExpenseItem.AgencyFee);
            otherServicesAndFees.ValueInLocalCurrency       = GetBillingExpenseItemValue(billingExpense, Constants.BillingExpenseItem.OtherCosts);

            var exchangeRates = await _costExchangeRateService.GetExchangeRatesByDefaultCurrency(costStageRevision.CostStage.CostId);

            UpdateDefaultCurrency(interpolatedItems, exchangeRates);

            return(interpolatedItems);
        }
示例#4
0
        private async Task <CostSectionTotals> GetTotals(Guid revisionId)
        {
            var revision = await _efContext.CostStageRevision.Include(x => x.CostStage)
                           .FirstOrDefaultAsync(x => x.Id == revisionId);

            var costLineItems = await _costStageRevisionService.GetCostLineItems(revisionId);

            var stageDetailsForm = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(revisionId);

            return(_pgTotalsBuilder.Build(stageDetailsForm, costLineItems, revision.CostStage.Key));
        }
示例#5
0
        public async Task AddUserToExistingCosts(CostUser user, UserBusinessRole userBusinessRole)
        {
            if (userBusinessRole.BusinessRole.Key == Constants.BusinessRole.RegionalAgencyUser && userBusinessRole.ObjectType == core.Constants.AccessObjectType.Region)
            {
                var costs = await _efContext.Cost
                            .Where(c => c.Owner.Agency.GlobalAgencyRegionId != null)
                            .Where(c => userBusinessRole.LocationIds.Contains(c.Owner.Agency.GlobalAgencyRegion.Id.ToString()))
                            .Select(c => new { costId = c.Id, agencyRegion = c.Owner.Agency.GlobalAgencyRegion.Region })
                            .ToListAsync();

                foreach (var cost in costs)
                {
                    await _permissionService.GrantUserAccess <Cost>(
                        userBusinessRole.BusinessRole.RoleId, cost.costId, user, BuType.Pg, null, cost.agencyRegion, false);
                }
            }
            else
            {
                var queryableCosts = GetQueryableCostsByUserObjectTypeAndLabels(userBusinessRole.ObjectType, userBusinessRole.Labels);
                if (queryableCosts == null)
                {
                    return;
                }

                var costs = await queryableCosts
                            .Include(c => c.LatestCostStageRevision)
                            .ThenInclude(csr => csr.StageDetails)
                            .ToArrayAsync();

                foreach (var cost in costs)
                {
                    var costStageDetails = _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(cost.LatestCostStageRevision);
                    switch (userBusinessRole.ObjectType)
                    {
                    case core.Constants.AccessObjectType.Smo:
                        if (!user.UserUserGroups.Any() || !user.UserUserGroups.Any(uug => uug.UserGroup.Label == costStageDetails.SmoName && uug.UserGroup.ObjectId == cost.Id))
                        {
                            await _permissionService.GrantUserAccess <Cost>(
                                userBusinessRole.BusinessRole.RoleId, cost.Id, user, BuType.Pg, null, costStageDetails.SmoName, false);
                        }
                        break;

                    case core.Constants.AccessObjectType.Region:
                        if (!user.UserUserGroups.Any() || user.UserUserGroups.Any(uug => uug.UserGroup.Label != costStageDetails.BudgetRegion.Name))
                        {
                            await _permissionService.GrantUserAccess <Cost>(
                                userBusinessRole.BusinessRole.RoleId, cost.Id, user, BuType.Pg, null, costStageDetails.BudgetRegion.Name, false);
                        }
                        break;
                    }
                }
            }
        }
示例#6
0
        public async Task <ICollection <MetadataItem> > Provide(Guid costId)
        {
            var metadataItems = new List <MetadataItem>();

            if (costId == Guid.Empty)
            {
                Logger.Warning("EN001: CostId is empty Guid.");
                return(metadataItems);
            }

            var cost = await _efContext.Cost
                       .AsNoTracking()
                       .Include(c => c.Owner)
                       .ThenInclude(cu => cu.Agency)
                       .ThenInclude(a => a.Country)
                       .Include(c => c.LatestCostStageRevision)
                       .ThenInclude(csr => csr.Approvals)
                       .ThenInclude(a => a.ApprovalMembers)
                       .ThenInclude(am => am.CostUser)
                       .Include(c => c.LatestCostStageRevision)
                       .ThenInclude(csr => csr.StageDetails)
                       .Include(c => c.Project)
                       .ThenInclude(p => p.Brand)
                       .FirstOrDefaultAsync(c => c.Id == costId);

            if (cost == null)
            {
                Logger.Warning($"EN002: Cost with Id {costId} not found.");
                return(metadataItems);
            }

            var costOwner = cost.Owner;

            if (costOwner == null)
            {
                Logger.Warning($"EN003: CostOwner with Id {cost.OwnerId} not found.");
                return(metadataItems);
            }

            var stageDetails = _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(cost.LatestCostStageRevision);

            if (stageDetails == null)
            {
                Logger.Warning($"EN004: Stage Details for Cost Id {costId} not found.");
                return(metadataItems);
            }

            AddMetadataItems(cost, costOwner, stageDetails, metadataItems);

            return(metadataItems);
        }
        protected void PopulateOtherFields(EmailNotificationMessage <CostNotificationObject> message, string parent, DateTime timestamp, Guid costId, Guid costStageRevisionId, CostUser approvalCostUser = null)
        {
            message.Timestamp = timestamp;

            CostNotificationObject obj = message.Object;

            core.Models.Notifications.Cost cost = obj.Cost;

            PgStageDetailsForm stageForm = CostStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevisionId).Result;

            cost.Title = stageForm.Title;
            switch (cost.CostType)
            {
            case core.Models.CostTemplate.CostType.Production:
                cost.ProductionType = stageForm.ProductionType?.Key;
                cost.ContentType    = stageForm.ContentType.Key;
                break;

            case core.Models.CostTemplate.CostType.Buyout:
                cost.ProductionType = stageForm.UsageBuyoutType.Key;
                cost.ContentType    = stageForm.UsageType?.Key;
                break;
            }

            cost.Url = _uriHelper.GetLink(ApplicationUriName.CostRevisionReview, costStageRevisionId.ToString(), costId.ToString());

            AssignApproverType(message, approvalCostUser);

            if (!string.IsNullOrEmpty(parent))
            {
                obj.Parents.Add(parent);
            }
            if (!string.IsNullOrEmpty(AppSettings.EnvironmentEmailSubjectPrefix))
            {
                obj.EnvironmentEmailSubjectPrefix = AppSettings.EnvironmentEmailSubjectPrefix;
            }
        }
示例#8
0
        public async Task <IEnumerable <DictionaryEntry> > GetMediaTypes(Guid costStageRevisionId)
        {
            var costStageRevision = await _efContext.CostStageRevision
                                    .Include(csr => csr.StageDetails)
                                    .Include(csr => csr.CostStage)
                                    .ThenInclude(cs => cs.Cost)
                                    .FirstOrDefaultAsync(csr => csr.Id == costStageRevisionId);

            if (costStageRevision == null)
            {
                throw new Exception($"Couldn't find costStageRevision with Id {costStageRevisionId}");
            }

            var cost = costStageRevision.CostStage.Cost;

            var stageDetails = _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevision);

            List <DictionaryEntry> entries;

            switch (cost.CostType)
            {
            case CostType.Production:
                entries = await _efContext.DependentItem
                          .Where(di => di.ParentId == stageDetails.ContentType.Id)
                          .Join(_efContext.DictionaryEntry, di => di.ChildId, de => de.Id, (di, de) => de)
                          .ToListAsync();

                break;

            case CostType.Buyout:
                entries = await _efContext.DependentItem
                          .Where(di => di.ParentId == stageDetails.UsageBuyoutType.Id)
                          .Join(_efContext.DictionaryEntry, di => di.ChildId, de => de.Id, (di, de) => de)
                          .ToListAsync();

                break;

            default:
                entries = await _efContext.DictionaryEntry
                          .Where(de =>
                                 de.Dictionary.Name == Constants.DictionaryNames.MediaType &&
                                 de.Key == Constants.MediaType.NA)
                          .ToListAsync();

                break;
            }

            return(entries);
        }
示例#9
0
        private async Task PopulateOtherFieldsForRecall(EmailNotificationMessage <CostNotificationObject> message, string parent, DateTime timestamp, Guid costId, Guid costStageRevisionId)
        {
            PopulateOtherFields(message, parent, timestamp, costId, costStageRevisionId);

            // Add fields specific to Recall notification message
            var obj           = message.Object;
            var cost          = obj.Cost;
            var stageForm     = _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevisionId).Result;
            var buyoutDetails = _costFormService.GetCostFormDetails <BuyoutDetails>(costStageRevisionId).Result;

            cost.AgencyTrackingNumber = stageForm.AgencyTrackingNumber;
            cost.Region          = stageForm.BudgetRegion?.Name;
            cost.AiringCountries = string.Join(";", (buyoutDetails?.AiringCountries ?? new BuyoutDetails.Country[0]).Select(c => c.Name).ToArray());
            cost.Requisition     = (await _customObjectDataService.GetCustomData <PgPurchaseOrderResponse>(costStageRevisionId, CustomObjectDataKeys.PgPurchaseOrderResponse))?.Requisition;
        }
        public async Task <CostStageRevisionStatus> GetNextStatus(Guid costId, CostAction action)
        {
            var cost = await _efContext.Cost
                       .Include(c => c.LatestCostStageRevision)
                       .ThenInclude(csr => csr.Approvals)
                       .ThenInclude(a => a.ApprovalMembers)
                       .Include(c => c.Project)
                       .Include(c => c.Parent)
                       .ThenInclude(p => p.Agency)
                       .FirstOrDefaultAsync(c => c.Id == costId);

            _logger.Information($"Working out next status for the cost {cost.CostNumber} {cost.Id}. Current status: {cost.Status} Action: {action}");

            var stageDetails = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(cost.LatestCostStageRevision.Id);

            var rules = await _ruleService.GetCompiledByRuleType <PgStatusRule>(RuleType.Status);

            var testRule = new PgStatusRule
            {
                Status               = cost.LatestCostStageRevision.Status.ToString(),
                BudgetRegion         = stageDetails.BudgetRegion?.Key,
                Action               = action.ToString(),
                CostType             = cost.CostType.ToString(),
                IsCyclone            = cost.Parent.Agency.IsCyclone(),
                HasTechnicalApproval = cost.LatestCostStageRevision.Approvals.Any(a => a.Type == ApprovalType.IPM),
                HasBrandApproval     = cost.LatestCostStageRevision.Approvals.Any(a => a.Type == ApprovalType.Brand),
                CostStage            = cost.LatestCostStageRevision.Name
            };

            if (_ruleService.TryMatchRule(rules, testRule, (r, dr) => JsonConvert.DeserializeObject <PgStatusRuleDefinition>(dr.Definition).Status, out var status))
            {
                _logger.Information($"Next cost status for cost {cost.CostNumber} is {status}. Previous status {cost.Status}");
                return(status);
            }

            throw new Exception($"Couldn't find status transition for cost {cost.CostNumber} rule: {JsonConvert.SerializeObject(testRule)}");
        }
        public async Task <ServiceResult <BillingExpenseViewModel> > Get(Guid costStageRevisionId)
        {
            //Get the contract period
            var usageForm = await _costFormService.GetCostFormDetails <BuyoutDetails>(costStageRevisionId);

            var contract  = usageForm.Contract;
            var startDate = contract.StartDate;
            var endDate   = contract.EndDate ?? DateTime.UtcNow;

            //Build the financial years based on start and end date
            var financialYears = await _financialYearService.Calculate(BuType.Pg, startDate, endDate);

            var model = new BillingExpenseViewModel();

            //Get the payment currency from the stage details form
            var stageForm = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevisionId);

            var paymentCurrency = await _efContext.Currency.FirstOrDefaultAsync(c => c.Code == stageForm.AgencyCurrency);

            model.PaymentCurrency = paymentCurrency;

            var billingExpenses = await _efContext.BillingExpense.Where(be => be.CostStageRevisionId == costStageRevisionId).ToListAsync();

            //Build the billing expenses
            var costStage = await _efContext.CostStageRevision
                            .Where(c => c.Id == costStageRevisionId)
                            .Select(c => c.CostStage).FirstOrDefaultAsync();

            model.Data     = _builder.BuildExpenses(costStage, billingExpenses, financialYears.Result);
            model.Modified = billingExpenses.Count > 0 ? billingExpenses.Max(b => b.Modified) : null;
            model.Saved    = model.Modified != null;

            //Perform any calculations
            await _calculator.CalculateExpenses(usageForm.Contract.ContractTotal, model.Data, billingExpenses);

            return(ServiceResult <BillingExpenseViewModel> .CreateSuccessfulResult(model));
        }
        public async Task <ServiceResult <BudgetFormUploadResult> > UploadBudgetForm(Guid costId, Guid costStageId, Guid costStageRevisionId, UserIdentity userIdentity, IFormFile file)
        {
            //Read the properties from the Properties excel sheet.
            ExcelProperties properties;

            using (var fileStream = file.OpenReadStream())
            {
                properties = _excelCellService.ReadProperties(file.FileName, fileStream);
            }

            //Validate the uploaded budget form is the correct format for the cost being updated.
            var stageForm = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevisionId);

            string contentType      = stageForm.GetContentType();
            string production       = stageForm.GetProductionType();
            var    validationResult = _budgetFormValidator.IsValid(properties, contentType, production);

            if (!validationResult.Success)
            {
                return(ServiceResult <BudgetFormUploadResult> .CloneFailedResult(validationResult));
            }

            //Read the cost line items
            ExcelCellValueLookup entries;

            using (var fileStream = file.OpenReadStream())
            {
                entries = await _excelCellService.ReadEntries(properties[core.Constants.BudgetFormExcelPropertyNames.LookupGroup], file.FileName, fileStream);
            }

            //Update the currencies in the Cost
            var currencyResult = await _costCurrencyUpdater.Update(entries, userIdentity.Id, costId, costStageRevisionId);

            if (!currencyResult.Success)
            {
                return(ServiceResult <BudgetFormUploadResult> .CloneFailedResult(currencyResult));
            }

            //Update the cost
            var costLineItemResult = await _costLineItemUpdater.Update(entries, userIdentity, costId, costStageRevisionId);

            if (!costLineItemResult.Success)
            {
                return(ServiceResult <BudgetFormUploadResult> .CloneFailedResult(costLineItemResult));
            }

            //Save updates made by _costLineItemUpdater and _costCurrencyUpdater.
            await _efContext.SaveChangesAsync();

            //Add Budget form to GDN using Gdam Core.
            var supportingDocument = await _supportingDocumentsService.GetSupportingDocument(costStageRevisionId, core.Constants.SupportingDocuments.BudgetForm);

            await _supportingDocumentsService.UploadSupportingDocumentRevision(costId, supportingDocument, userIdentity, file);

            var supportingDocumentViewModel = _mapper.Map <SupportingDocumentViewModel>(supportingDocument);

            var costLineItemModels = _mapper.Map <List <CostLineItemViewModel> >(costLineItemResult.Result);

            var budgetFormUploadResult = new BudgetFormUploadResult
            {
                Currencies         = currencyResult.Result,
                CostLineItems      = costLineItemModels,
                SupportingDocument = supportingDocumentViewModel
            };

            var costNumber = await _efContext.Cost.Where(c => c.Id == costId).Select(c => c.CostNumber).SingleAsync();

            await _activityLogService.Log(new BudgetFormUploaded(costNumber, file.FileName, supportingDocument.Id, userIdentity));

            return(new ServiceResult <BudgetFormUploadResult>(budgetFormUploadResult));
        }
示例#13
0
        public async Task <ServiceResult <List <CostLineItem> > > Update(ExcelCellValueLookup entries, UserIdentity userIdentity, Guid costId, Guid costStageRevisionId)
        {
            if (entries == null)
            {
                Logger.Warning("Param entries is null reference. This means the uploaded budget form was not read correctly.");
                return(ServiceResult <List <CostLineItem> > .CreateFailedResult(TechnicalErrorMessage));
            }
            if (userIdentity == null)
            {
                Logger.Warning("Param userIdentity is null reference.");
                return(ServiceResult <List <CostLineItem> > .CreateFailedResult(TechnicalErrorMessage));
            }
            if (costId == Guid.Empty)
            {
                Logger.Warning("Param costId is Guid.Empty.");
                return(ServiceResult <List <CostLineItem> > .CreateFailedResult(TechnicalErrorMessage));
            }
            if (costStageRevisionId == Guid.Empty)
            {
                Logger.Warning("Param costStageRevisionId is Guid.Empty.");
                return(ServiceResult <List <CostLineItem> > .CreateFailedResult(TechnicalErrorMessage));
            }
            if (entries.Count == 0)
            {
                Logger.Warning("Param entries is empty collection. This means the uploaded budget form was not read correctly.");
                return(ServiceResult <List <CostLineItem> > .CreateFailedResult(TechnicalErrorMessage));
            }

            var updatedItems  = new List <CostLineItem>();
            var serviceResult = new ServiceResult <List <CostLineItem> >(updatedItems);

            var stageForm = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevisionId);

            var contentType = stageForm.GetContentType();
            var production  = stageForm.GetProductionType();

            //Get the form
            var cost = await _efContext.Cost
                       .Include(c => c.CostTemplateVersion)
                       .ThenInclude(ctv => ctv.CostTemplate)
                       .ThenInclude(ct => ct.FieldDefinitions)
                       .FirstOrDefaultAsync(c => c.Id == costId);

            var costStageRevision = await _efContext.CostStageRevision
                                    .Include(c => c.CostLineItems)
                                    .FirstOrDefaultAsync(c => c.Id == costStageRevisionId);

            var templateModel = await _costTemplateVersionService.GetCostTemplateVersionModel(cost.CostTemplateVersionId);

            var formResult = _costSectionFinder.GetCostSection(templateModel, contentType, production);

            if (!formResult.Success)
            {
                serviceResult.AddErrorRange(formResult.Errors);
                return(serviceResult);
            }

            var form = formResult.Result;

            // SPB-3005 --> ADC-2892
            var exchangeRates = await _costExchangeRateService.GetExchangeRatesByDefaultCurrency(costId);

            //Iterate through the cost line items
            foreach (var costSection in form.CostLineItemSections)
            {
                foreach (var item in costSection.Items)
                {
                    //Build a lookup key that will look in the LookupKey column for an exact match.
                    // lookupKey format is form.costSection.item.currency or costSection.item.currency
                    var defaultLookupKey = GetDefaultCurrencyLookupKey(form, costSection, item);
                    var localLookupKey   = GetLocalCurrencyLookupKey(form, costSection, item);

                    if (!entries.ContainsKey(defaultLookupKey))
                    {
                        // if the lookupKey does not exist with the form.Name included, try without the form.
                        var message = $"BF001: {form.Name}:{costSection.Name}:{item.Name} not found for cost: {costId} in uploaded budget form. Trying {costSection.Name}:{item.Name}...";
                        Logger.Warning(message);
                        defaultLookupKey = GetDefaultCurrencyLookupKey(costSection, item);
                    }

                    if (!entries.ContainsKey(localLookupKey))
                    {
                        localLookupKey = GetLocalCurrencyLookupKey(costSection, item);
                    }

                    if (entries.ContainsKey(defaultLookupKey) && entries.ContainsKey(localLookupKey))
                    {
                        var defaultCurrencyStringValue = entries[defaultLookupKey].Value;
                        var localCurrencyStringValue   = entries[localLookupKey].Value;
                        var updateCell = true;
                        if (!decimal.TryParse(localCurrencyStringValue, out var localCurrencyValue))
                        {
                            var warning = $"BF002: Invalid entry '{localCurrencyStringValue}' for {costSection.Name}:{item.Name} for cost: {costId} in uploaded budget form.";
                            Logger.Warning(warning);

                            var userError = $"Invalid entry '{localCurrencyStringValue}' in cell '{entries[localLookupKey].Name}'.";
                            serviceResult.AddError(userError);
                            updateCell = false;
                        }
                        if (!decimal.TryParse(defaultCurrencyStringValue, out var defaultCurrencyValue))
                        {
                            var warning = $"BF003: Invalid entry '{defaultCurrencyStringValue}' for {costSection.Name}:{item.Name} for cost: {costId} in uploaded budget form.";
                            Logger.Warning(warning);

                            var userError = $"Invalid entry '{defaultCurrencyStringValue}' in cell '{entries[defaultLookupKey].Name}'.";
                            serviceResult.AddError(userError);
                            updateCell = false;
                        }

                        if (updateCell)
                        {
                            var cli      = GetOrCreateCostLineItem(costStageRevision, userIdentity.Id, costSection, item);
                            var currency = await GetCurrency(entries, costSection, item);

                            if (HasCurrencyChanged(currency, cli) && !CanUpdateCurrency(costStageRevision))
                            {
                                var costSectionCurrency = await GetCurrencyCode(cli.LocalCurrencyId);

                                var error = new FeedbackMessage(
                                    $"The currency you have chosen in the budget form, {currency.Code} does not match the cost section currency of {costSectionCurrency}. There are two options to progress this:");
                                error.AddSuggestion("You may change the currency of your budget form to match the cost section currency and re-upload the Budget form.");
                                error.AddSuggestion(
                                    "You may cancel this datasheet, create a new cost and select the required currency. After cancelling your cost please contact P&G. They will raise and issue credit for any monies you have received against the original PO.");
                                serviceResult.AddError(error);

                                return(serviceResult);
                            }

                            //Update the cost line item
                            cli.LocalCurrencyId = currency.Id;
                            var exchangeRate = exchangeRates.FirstOrDefault(ex => ex.FromCurrency == cli.LocalCurrencyId);
                            cli.ValueInDefaultCurrency = (exchangeRate?.Rate * localCurrencyValue) ?? 0; //we calculate it because we can't trust the sent value from front-end
                            cli.ValueInLocalCurrency   = localCurrencyValue;
                            cli.SetModifiedNow();
                            updatedItems.Add(cli);
                        }
                    }
                    else
                    {
                        var userError = $"BF004: {costSection.Name}:{item.Name} not found for cost: {costId} in uploaded budget form. Adding Zero as default value.";
                        Logger.Warning(userError);
                        serviceResult.AddWarning(userError);

                        //Create a default value
                        if (item.Mandatory.HasValue && item.Mandatory.Value)
                        {
                            var cli      = GetOrCreateCostLineItem(costStageRevision, userIdentity.Id, costSection, item);
                            var currency = await GetCurrency(entries, costSection, item);

                            //Update the cost line item
                            cli.LocalCurrencyId        = currency.Id;
                            cli.ValueInDefaultCurrency = 0;
                            cli.ValueInLocalCurrency   = 0;
                            cli.SetModifiedNow();
                            updatedItems.Add(cli);
                        }
                    }
                }
            }
            return(serviceResult);
        }
        public async Task <PaymentAmountResult> CalculatePaymentAmount(Guid costStageRevisionId, bool persist = true)
        {
            var revision = await _efContext.CostStageRevision
                           .Include(csr => csr.CostFormDetails)
                           .Include(r => r.CostStage).ThenInclude(cs => cs.Cost)
                           .Include(r => r.CostStage).ThenInclude(cs => cs.CostStageRevisions)
                           .Include(r => r.StageDetails)
                           .Include(r => r.ProductDetails)
                           .Include(r => r.CostStageRevisionPaymentTotals)
                           //.AsNoTracking()
                           .FirstOrDefaultAsync(csr => csr.Id == costStageRevisionId);

            var costStage = revision.CostStage;
            var cost      = costStage.Cost;

            var stageDetailsForm      = _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(revision);
            var productionDetailsForm = _costStageRevisionService.GetProductionDetails <PgProductionDetailsForm>(revision);

            //ADC-2690 revises paymentCostTotal calculation for re-opened Final Actual stage
            var previousPaymentCalculations = new List <CostStageRevisionPaymentTotal>();
            //get the latest costtotal from last approved final actual
            var previousPaymentCostTotal = new CostStageRevisionPaymentTotal();
            //flag to determine if this calculation is for the first FA or subsequent FAs
            bool isFirstFA = true;
            //check if the current cost has any Final Actual stage approved
            var approvedFinalActualStage = cost.CostStages.Find(x => x.Key == CostStages.FinalActual.ToString())?.CostStageRevisions.Find(a => a.Status == CostStageRevisionStatus.Approved);

            //if there is no final actual stage approve, then keep the current calculation as is, which is working correctly.
            if (approvedFinalActualStage == null)
            {
                previousPaymentCalculations = await _costStageRevisionService.GetAllCostPaymentTotals(cost.Id, revision.CostStage.Id);
            }
            else
            {
                //here is the area we do the calculation for re-opened FAs
                //Get All Cost Payment Totals for the current Final Actual stage
                previousPaymentCalculations = await _costStageRevisionService.GetAllCostPaymentTotalsFinalActual(cost.Id, revision.CostStage.Id);

                //extract values of CostTotal rowns of approved FA and order by calculated datetime
                var previousPaymentCostTotals = previousPaymentCalculations.Where(x => x.LineItemTotalType == Constants.CostSection.CostTotal &&
                                                                                  x.CostStageRevision.Status == CostStageRevisionStatus.Approved)
                                                .OrderBy(x => x.CalculatedAt).ToList();
                //check if there is at least 1 approved FA
                if (previousPaymentCalculations.Any() && previousPaymentCostTotals.Any())
                {
                    //if there is an approved FA, it means there is an inprogress FA, and we shall need to get the last FA for subtraction later: Grand total at Final actual -II minus Grand total in Final actual -I
                    previousPaymentCostTotal = previousPaymentCostTotals[previousPaymentCostTotals.Count() - 1];
                    //flag up this is not the first FA
                    isFirstFA = false;
                }
                else
                {
                    //otherwise, keep the calculation as is
                    previousPaymentCalculations = await _costStageRevisionService.GetAllCostPaymentTotals(cost.Id, revision.CostStage.Id);
                }
            }

            var costLineItems = await _costStageRevisionService.GetCostLineItems(costStageRevisionId);

            var totals = _pgTotalsBuilder.Build(stageDetailsForm, costLineItems, costStage.Key);
            var previousPaymentTotals = _pgTotalPaymentsBuilder.Build(previousPaymentCalculations);

            // these are totals of remaining balance
            //changed for ADC-2690
            var totalRemainingPayment = new PgPaymentRule()
            {
                StageTotals           = totals,
                BudgetRegion          = stageDetailsForm.BudgetRegion?.Key,
                ContentType           = stageDetailsForm.ContentType?.Key,
                CostType              = cost.CostType.ToString(),
                CostStages            = costStage.Key,
                ProductionType        = Constants.ProductionType.ProductionTypeList.FirstOrDefault(a => a == stageDetailsForm.ProductionType?.Key),
                DirectPaymentVendorId = productionDetailsForm.DirectPaymentVendor?.Id,
                IsAIPE = stageDetailsForm.IsAIPE,

                // we need this to match with the rules' TotalCostAmount field
                TotalCostAmount = totals.TotalCostAmountTotal,

                // this is for detailed split
                InsuranceCost      = totals.InsuranceCostTotal - previousPaymentTotals.InsuranceCostPayments,
                TechnicalFeeCost   = totals.TechnicalFeeCostTotal - previousPaymentTotals.TechnicalFeeCostPayments,
                TalentFeeCost      = totals.TalentFeeCostTotal - previousPaymentTotals.TalentFeeCostPayments,
                PostProductionCost = totals.PostProductionCostTotal - previousPaymentTotals.PostProductionCostPayments,
                ProductionCost     = totals.ProductionCostTotal - previousPaymentTotals.ProductionCostPayments,
                OtherCost          = totals.OtherCostTotal - previousPaymentTotals.OtherCostPayments,

                TargetBudgetTotalCost = totals.TargetBudgetTotal - previousPaymentTotals.TargetBudgetTotalCostPayments,
                CostCarryOverAmount   = previousPaymentTotals.CarryOverAmount
            };

            //check if this is not the calculation for the first FA then do the subtraction: Grand total at Final actual -II minus Grand total in Final actual -I
            //if not keep as is
            if (!isFirstFA)
            {
                //if this is not the first FA, it means we would have to calculated TotalCost AKA CostTotal equal Grand total at Final actual -II minus Grand total in Final actual -I
                totalRemainingPayment.TotalCost = totals.TotalCostAmountTotal - previousPaymentCostTotal.LineItemFullCost;
            }
            else
            {
                // we use this to calculate the outstanding balance where there is no detailed split
                totalRemainingPayment.TotalCost = totals.TotalCostAmountTotal - previousPaymentTotals.TotalCostPayments;
            }
            _logger.Information($"Calculating payment amount for cost: {cost.CostNumber} at stage: {costStage.Key} revision: {revision.Id}");

            // these are actual payment splits (percentages of totals)
            var paymentAmount = await GetPaymentAmount(totalRemainingPayment, previousPaymentTotals, totals, productionDetailsForm.DirectPaymentVendor?.ProductionCategory);

            if (paymentAmount != null)
            {
                if (!persist)
                {
                    return(paymentAmount);
                }
                var totalRemainingAmount = (PgPaymentRule)paymentAmount.TotalRemainingPayment;

                var nextStages = await _stageService.GetAllUpcomingStages(costStage.Key, BuType.Pg, cost.Id);

                if (nextStages != null)
                {
                    paymentAmount.ProjectedPayments = GetNextStagesPaymentAmounts(paymentAmount, totalRemainingPayment, nextStages);
                }

                var alreadySaved = await _costStageRevisionService.GetCostStageRevisionPaymentTotals(revision);

                if (alreadySaved == null || !alreadySaved.Any())
                {
                    await SaveTotals(costStageRevisionId, paymentAmount, totalRemainingAmount, isFirstFA);

                    foreach (var projectedPayment in paymentAmount.ProjectedPayments)
                    {
                        await SaveTotals(costStageRevisionId, projectedPayment, totalRemainingAmount, isFirstFA);
                    }
                }
                // UserIdentity in the parameters of below method is used only to log an activity when IO number gets changed.
                // Therefore we can pass any not null object here
                await _customObjectDataService.Save(costStageRevisionId, CustomObjectDataKeys.PgPaymentRuleInput, totalRemainingPayment, new UserIdentity());

                await _customObjectDataService.Save(costStageRevisionId, CustomObjectDataKeys.PgMatchedPaymentRule, paymentAmount.MatchedPaymentRule, new UserIdentity());

                return(paymentAmount);
            }

            _logger.Error($"Payment amount NOT calculated for cost: {cost.CostNumber} at stage: {costStage.Key} revision: {revision.Id} using rule: {totalRemainingPayment}!");
            return(null);
        }
        public async Task <ServiceResult <Dictionary <string, Guid> > > Update(ExcelCellValueLookup entries, Guid userId, Guid costId, Guid costStageRevisionId)
        {
            if (entries == null)
            {
                Logger.Warning("Param entries is null reference. This means the uploaded budget form was not read correctly.");
                return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage));
            }
            if (userId == Guid.Empty)
            {
                Logger.Warning("Param userId is Guid.Empty.");
                return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage));
            }
            if (costId == Guid.Empty)
            {
                Logger.Warning("Param costId is Guid.Empty.");
                return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage));
            }
            if (costStageRevisionId == Guid.Empty)
            {
                Logger.Warning("Param costStageRevisionId is Guid.Empty.");
                return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage));
            }
            if (entries.Count == 0)
            {
                Logger.Warning("Param entries is empty collection. This means the uploaded budget form was not read correctly.");
                return(ServiceResult <Dictionary <string, Guid> > .CreateFailedResult(TechnicalErrorMessage));
            }

            var stageForm = await _costStageRevisionService.GetStageDetails <PgStageDetailsForm>(costStageRevisionId);

            var productionForm = await _costStageRevisionService.GetProductionDetails <PgProductionDetailsForm>(costStageRevisionId);

            var costStageRevision = await _efContext.CostStageRevision
                                    .Include(csr => csr.CostStage)
                                    .ThenInclude(csr => csr.CostStageRevisions)
                                    .FirstOrDefaultAsync(c => c.Id == costStageRevisionId);

            //Update the Agency Currency, if different
            var agencyCurrencyCode = GetAgencyCurrencyCode(entries);

            // return a dictionary of the currencies that have been modified i.e. agency, dpv, section.
            // Key is agency, dpv or section name and value is the currencyId. The FE has all the Ids and can update itself.
            var currencies    = new Dictionary <string, Guid>();
            var serviceResult = new ServiceResult <Dictionary <string, Guid> >(currencies);

            if (stageForm.AgencyCurrency != agencyCurrencyCode)
            {
                if (CanUpdateAgencyCurrency(costStageRevision))
                {
                    var stageDetails = await _efContext.CustomFormData.FirstOrDefaultAsync(c => c.Id == costStageRevision.StageDetailsId);

                    UpdateAgencyCurrency(stageDetails, agencyCurrencyCode);
                    var agencyCurrencyId = await GetCurrencyId(agencyCurrencyCode);

                    currencies[AgencyCurrencyKey] = agencyCurrencyId;
                }
                else
                {
                    var error = new FeedbackMessage(
                        $"The agency currency you have chosen in the budget form, {agencyCurrencyCode} does not match your datasheet of {stageForm.AgencyCurrency}. There are two options to progress this:");
                    error.AddSuggestion("You may change the currency of your budget form to match the datasheet currency and re-upload the Budget form.");
                    error.AddSuggestion(
                        "You may cancel this datasheet, create a new cost and select the required agency currency. After cancelling your cost please contact P&G. They will raise and issue credit for any monies you have received against the original PO.");
                    serviceResult.AddError(error);

                    return(serviceResult);
                }
            }

            // The Cost Section Currencies done by the ICostLineItemUpdater because each CostLineItem has a currency.
            // On the UI, the Cost Section currency is set by rolling up from the first CostLineItem.

            // Here we extrapolate the currencies for the DPV and the cost line item sections in order to simplify the
            // front end code. At this present time, the FE calculates the CLI Section currency based on the first item it finds.
            if (productionForm.DirectPaymentVendor != null)
            {
                var dpvCurrencyCode = GetDpvCurrencyCode(entries, stageForm);
                var dpvCurrencyId   = await GetCurrencyId(dpvCurrencyCode);

                var productionDetails = await _efContext.CustomFormData.FirstOrDefaultAsync(c => c.Id == costStageRevision.ProductDetailsId);

                UpdateDpvCurrency(productionDetails, dpvCurrencyId);
                currencies[DirectPaymentVendorCurrencyKey] = dpvCurrencyId;
            }

            return(serviceResult);
        }