Esempio n. 1
0
        public async Task <OperationResponse> IsValidForSubmittion(Guid costId)
        {
            var cost = await _efContext.Cost
                       .Include(c => c.Parent)
                       .ThenInclude(p => p.Agency)
                       .Include(c => c.LatestCostStageRevision)
                       .ThenInclude(r => r.ProductDetails)
                       .FirstOrDefaultAsync(c => c.Id == costId);

            if (cost == null)
            {
                throw new EntityNotFoundException <Cost>(costId);
            }

            var productionDetails = _costStageRevisionService.GetProductionDetails <PgProductionDetailsForm>(cost.LatestCostStageRevision);
            var isDpv             = productionDetails?.DirectPaymentVendor != null;
            var sapCode           = isDpv
                ? productionDetails.DirectPaymentVendor.SapVendorCode
                : cost.Parent.Agency.Labels.GetSapVendorCode();

            var isValid      = !string.IsNullOrWhiteSpace(sapCode);
            var errorMessage = isValid ? string.Empty : MissingVendorErrorMessage;

            return(new OperationResponse(isValid, errorMessage));
        }
        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);
        }