public async Task GetCostStagesLatestRevisions_2ApprovedFAs() { // Arrange var currency = new Currency { Code = "USD", DefaultCurrency = false, Description = "", Id = Guid.NewGuid(), Symbol = "$" }; var cost = new Cost { Id = Guid.NewGuid(), PaymentCurrency = currency }; var costStageRevisionId = Guid.NewGuid(); const string costNumber = "PGU0000001V0029"; var costStageOe = new CostStage { Id = Guid.NewGuid(), CostId = cost.Id, CreatedById = Guid.NewGuid(), Created = DateTime.Now, Modified = DateTime.Now, Key = CostStages.OriginalEstimate.ToString(), StageOrder = 1, Cost = cost }; var costStageFP = new CostStage { Id = Guid.NewGuid(), CostId = cost.Id, CreatedById = Guid.NewGuid(), Created = DateTime.Now, Modified = DateTime.Now, Key = CostStages.FirstPresentation.ToString(), StageOrder = 2 }; var costStageFA = new CostStage { Id = Guid.NewGuid(), CostId = cost.Id, CreatedById = Guid.NewGuid(), Created = DateTime.Now, Modified = DateTime.Now, Key = CostStages.FinalActual.ToString(), StageOrder = 3 }; var costStageRevisionFA1 = new CostStageRevision { Id = Guid.NewGuid(), StageDetailsId = costStageRevisionId, StageDetails = new CustomFormData { Id = costStageRevisionId, Data = JsonConvert.SerializeObject(new PgStageDetailsForm { CostNumber = costNumber }, _serializerSettings) }, CostStage = costStageFA, Status = CostStageRevisionStatus.Approved, IsLineItemSectionCurrencyLocked = true, IsPaymentCurrencyLocked = true }; var costStageRevisionFA2 = new CostStageRevision { Id = Guid.NewGuid(), StageDetailsId = costStageRevisionId, StageDetails = new CustomFormData { Id = costStageRevisionId, Data = JsonConvert.SerializeObject(new PgStageDetailsForm { CostNumber = costNumber }, _serializerSettings) }, CostStage = costStageFA, Status = CostStageRevisionStatus.PendingTechnicalApproval, IsLineItemSectionCurrencyLocked = true, IsPaymentCurrencyLocked = true }; var costStageRevisionPaymentTotalFA1 = new CostStageRevisionPaymentTotal { Id = Guid.NewGuid(), CostStageRevision = costStageRevisionFA1, LineItemTotalType = "CostTotal", LineItemFullCost = 41000, LineItemTotalCalculatedValue = 25500, IsProjection = false, StageName = CostStages.FinalActual.ToString(), CalculatedAt = DateTime.Now }; var costStageRevisionPaymentTotalFA2 = new CostStageRevisionPaymentTotal { Id = Guid.NewGuid(), CostStageRevision = costStageRevisionFA2, LineItemTotalType = "CostTotal", LineItemFullCost = 35000, LineItemTotalCalculatedValue = -6000,//35000-41000 IsProjection = false, StageName = CostStages.FinalActual.ToString(), CalculatedAt = DateTime.Now }; _efContext.CostStage.AddRange(costStageOe, costStageFP, costStageFA); _efContext.CostStageRevision.AddRange(costStageRevisionFA1, costStageRevisionFA2); _efContext.Currency.Add(currency); _efContext.SaveChanges(); var costStageRevisionPaymentTotals = new List <CostStageRevisionPaymentTotal> { costStageRevisionPaymentTotalFA1, costStageRevisionPaymentTotalFA2 }; CostStageRevisionServiceMock.Setup(a => a.GetLatestRevisionWithPaymentCurrency(It.IsAny <Guid>())).ReturnsAsync(costStageRevisionFA2); CostStageRevisionServiceMock.Setup(a => a.GetAllCostPaymentTotalsFinalActual(It.IsAny <Guid>(), It.IsAny <Guid>())).ReturnsAsync(costStageRevisionPaymentTotals); // Act var result = await CostStageService.GetStagesLatestRevision(cost.Id, BuType.Pg); // Assert result.Should().HaveCount(3); ((string)result[2].StageDetails.costNumber).Should().Be(costNumber); result[2].LatestRevision.Should().Be(costStageRevisionFA2); result[2].DisplayGRAmount.Should().Be(19500); }
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); }