private decimal?CalculateLearningDeliveryEarningsToPeriod(
            List <LearningDeliveryEarning> ldLearnerRecords,
            bool yearToDate,
            int period,
            LearnerLevelViewModel learnerLevelViewModel)
        {
            // Exit if there are no records to calc
            if (ldLearnerRecords == null)
            {
                return(0);
            }

            decimal?sum       = 0;
            var     ldRecords = ldLearnerRecords?.Where(pe => _ldAttributeGroup.Contains(pe.AttributeName));

            foreach (var ld in ldRecords)
            {
                // Check to see if this is a request for "LearnSuppFundCash" - if it is check to see if the record has LearnDelMathEng flag set.
                // If it does, we are OK to use this value in the sum (otherwise the corresponding PE value will be used)
                if ((ld.AttributeName == AttributeConstants.Fm36LearnSuppFundCash) && (ld.LearnDelMathEng != true))
                {
                    // Don't calc anything
                }
                else
                {
                    sum = sum + LDPeriodSum(ld, period, yearToDate);
                }
            }

            return(sum);
        }
        private decimal?CalculatePriceEpisodeEarningsToPeriod(
            List <LearningDeliveryEarning> ldLearnerRecords,
            List <PriceEpisodeEarning> pelearnerRecords,
            bool yearToDate,
            int period,
            LearnerLevelViewModel learnerLevelViewModel)
        {
            // Exit if there are no records to calc
            if (pelearnerRecords == null)
            {
                return(0);
            }

            decimal?sum = 0;

            // Build a list of records we don't need to calculate for
            var mathEngRecords = ldLearnerRecords.Where(p => p.LearnDelMathEng == true).Select(record => new PriceEpisodeEarning()
            {
                LearnRefNumber = record.LearnRefNumber
            });

            var peRecords = pelearnerRecords?.Where(pe => _peAttributeGroup.Contains(pe.AttributeName)).Except(mathEngRecords, _peEarningComparer);

            foreach (var pe in peRecords)
            {
                sum = sum + PEPeriodSum(pe, period, yearToDate);
            }

            return(sum);
        }
        private decimal?CalculateLearningDeliveryEarningsToPeriod(
            List <AECLearningDeliveryPeriodisedValuesInfo> ldLearnerRecords,
            bool yearToDate,
            int period,
            LearnerLevelViewModel learnerLevelViewModel)
        {
            // Exit if there are no records to calc
            if (ldLearnerRecords == null)
            {
                return(0);
            }

            var ldAttributeGroup = new List <string>()
            {
                Generics.Fm36MathEngOnProgPaymentAttributeName,
                Generics.Fm36LearnSuppFundCashAttributeName,
                Generics.Fm36MathEngBalPayment,
                Generics.Fm36DisadvFirstPayment,
                Generics.Fm36DisadvSecondPayment,
                Generics.Fm36LearnDelFirstEmp1618Pay,
                Generics.Fm36LearnDelSecondEmp1618Pay,
                Generics.Fm36LearnDelFirstProv1618Pay,
                Generics.Fm36LearnDelSecondProv1618Pay,
                Generics.Fm36LearnDelLearnAddPayment,
                Generics.Fm36LDApplic1618FrameworkUpliftBalancingPayment,
                Generics.Fm36LDApplic1618FrameworkUpliftCompletionPayment,
                Generics.Fm36LDApplic1618FrameworkUpliftOnProgPayment
            };

            decimal?sum       = 0;
            var     ldRecords = ldLearnerRecords?.Where(pe => ldAttributeGroup.Contains(pe.AttributeName));

            foreach (var ld in ldRecords)
            {
                // Check to see if this is a request for "LearnSuppFundCash" - if it is check to see if the record has LearnDelMathEng flag set.
                // If it does, we are OK to use this value in the sum (otherwise the corresponding PE value will be used)
                if ((ld.AttributeName == Generics.Fm36LearnSuppFundCashAttributeName) && (ld.LearnDelMathEng != true))
                {
                    // Don't calc anything
                }
                else
                {
                    // Make sure this is the right attrib to be summed
                    // NOTE: "i" is an index value created in the linq query
                    if ((yearToDate == true) && (ld.Periods != null))
                    {
                        sum = sum + ld.Periods.Select((pv, i) => new { i, pv }).Where(a => a.i <= period - 1).Sum(o => o.pv);
                    }
                    else
                    {
                        sum = sum + ld.Periods.Select((pv, i) => new { i, pv }).Where(a => a.i == period - 1).Sum(o => o.pv);
                    }
                }
            }

            return(sum);
        }
        private decimal?CalculatePriceEpisodeEarningsToPeriod(
            List <AECLearningDeliveryPeriodisedValuesInfo> ldLearnerRecords,
            List <AECApprenticeshipPriceEpisodePeriodisedValuesInfo> pelearnerRecords,
            bool yearToDate,
            int period,
            LearnerLevelViewModel learnerLevelViewModel)
        {
            // Exit if there are no records to calc
            if (pelearnerRecords == null)
            {
                return(0);
            }

            var peAttributeGroup = new List <string>()
            {
                Generics.Fm36PriceEpisodeCompletionPaymentAttributeName,
                Generics.Fm36PriceEpisodeOnProgPaymentAttributeName,
                Generics.Fm3PriceEpisodeBalancePaymentAttributeName,
                Generics.Fm36PriceEpisodeLSFCashAttributeName
            };

            decimal?sum = 0;

            // Build a list of records we don't need to calculate for
            var mathEngRecords = ldLearnerRecords.Where(p => p.LearnDelMathEng == true).Select(record => new AECApprenticeshipPriceEpisodePeriodisedValuesInfo()
            {
                LearnRefNumber = record.LearnRefNumber,
                AimSeqNumber   = record.AimSeqNumber
            });

            var peRecords = pelearnerRecords?.Where(pe => peAttributeGroup.Contains(pe.AttributeName)).Except(mathEngRecords, new AECApprenticeshipPriceEpisodePeriodisedValuesInfoComparer());

            foreach (var pe in peRecords)
            {
                // Add the PE value to the sum
                // NOTE: "i" is an index value created in the linq query
                if ((yearToDate == true) && (pe.Periods != null))
                {
                    sum = sum + pe.Periods.Select((pv, i) => new { i, pv }).Where(a => a.i <= period - 1).Sum(o => o.pv);
                }
                else
                {
                    sum = sum + pe.Periods.Select((pv, i) => new { i, pv }).Where(a => a.i == period - 1).Sum(o => o.pv);
                }
            }

            return(sum);
        }
        public IReadOnlyList <LearnerLevelViewModel> BuildLearnerLevelViewModelList(
            int ukprn,
            AppsMonthlyPaymentILRInfo appsMonthlyPaymentIlrInfo,
            AppsCoInvestmentILRInfo appsCoInvestmentIlrInfo,
            LearnerLevelViewDASDataLockInfo learnerLevelViewDASDataLockInfo,
            LearnerLevelViewHBCPInfo learnerLevelHBCPInfo,
            LearnerLevelViewFM36Info learnerLevelDAsInfo,
            IDictionary <LearnerLevelViewPaymentsKey, List <AppsMonthlyPaymentDasPaymentModel> > paymentsDictionary,
            IDictionary <string, List <AECApprenticeshipPriceEpisodePeriodisedValuesInfo> > aECPriceEpisodeDictionary,
            IDictionary <string, List <AECLearningDeliveryPeriodisedValuesInfo> > aECLearningDeliveryDictionary,
            IDictionary <long, string> employerNameDictionary,
            int returnPeriod)
        {
            // cache the passed in data for use in the private 'Get' methods
            _appsReturnPeriod = returnPeriod;

            // this variable is the final report and is the return value of this method.
            List <LearnerLevelViewModel> learnerLevelViewModelList = null;

            try
            {
                // Lookup for employer IDs to employer name
                IDictionary <int, string> employerNamesToIDs = new Dictionary <int, string>();

                // Union the keys from the datasets being used to source the report
                var unionedKeys = UnionKeys(paymentsDictionary.Keys, aECPriceEpisodeDictionary.Keys, aECLearningDeliveryDictionary.Keys);

                // Populate the learner list using the ILR query first
                learnerLevelViewModelList = unionedKeys
                                            .OrderBy(o => o.LearnerReferenceNumber)
                                            .ThenBy(o => o.PaymentFundingLineType)
                                            .Select(record =>
                {
                    var reportRecord = new LearnerLevelViewModel()
                    {
                        Ukprn = ukprn,
                        PaymentLearnerReferenceNumber = record.LearnerReferenceNumber,
                        PaymentFundingLineType        = record.PaymentFundingLineType
                    };

                    // Extract ILR info
                    var ilrRecord = appsMonthlyPaymentIlrInfo.Learners.FirstOrDefault(p => p.LearnRefNumber == reportRecord.PaymentLearnerReferenceNumber);
                    if (ilrRecord != null)
                    {
                        reportRecord.FamilyName = ilrRecord.FamilyName;
                        reportRecord.GivenNames = ilrRecord.GivenNames;
                        reportRecord.PaymentUniqueLearnerNumber = ilrRecord.UniqueLearnerNumber;
                        if ((ilrRecord.LearnerEmploymentStatus != null) && (ilrRecord.LearnerEmploymentStatus.Count > 0))
                        {
                            reportRecord.LearnerEmploymentStatusEmployerId = ilrRecord.LearnerEmploymentStatus.Where(les => les?.Ukprn == ukprn &&
                                                                                                                     les.LearnRefNumber.CaseInsensitiveEquals(reportRecord.PaymentLearnerReferenceNumber) &&
                                                                                                                     les?.EmpStat == 10)
                                                                             .OrderByDescending(les => les?.DateEmpStatApp)
                                                                             .FirstOrDefault()?.EmpdId;
                        }
                    }

                    if (paymentsDictionary.TryGetValue(new LearnerLevelViewPaymentsKey(reportRecord.PaymentLearnerReferenceNumber, reportRecord.PaymentFundingLineType), out List <AppsMonthlyPaymentDasPaymentModel> paymentValues))
                    {
                        // Assign the amounts
                        reportRecord.PlannedPaymentsToYouToDate = paymentValues.Where(p => PeriodESFAPlannedPaymentsFSTypePredicateToPeriod(p, _appsReturnPeriod) ||
                                                                                      PeriodESFAPlannedPaymentsTTTypePredicateToPeriod(p, _appsReturnPeriod) ||
                                                                                      PeriodESFAPlannedPaymentsNonZPTypePredicateToPeriod(p, _appsReturnPeriod)).Sum(c => c.Amount ?? 0m);

                        reportRecord.ESFAPlannedPaymentsThisPeriod = paymentValues.Where(p => PeriodESFAPlannedPaymentsFSTypePredicate(p, _appsReturnPeriod) ||
                                                                                         PeriodESFAPlannedPaymentsTTTypePredicate(p, _appsReturnPeriod) ||
                                                                                         PeriodESFAPlannedPaymentsNonZPTypePredicate(p, _appsReturnPeriod)).Sum(c => c.Amount ?? 0m);

                        reportRecord.CoInvestmentPaymentsToCollectThisPeriod = paymentValues.Where(p => PeriodCoInvestmentDueFromEmployerPaymentsTypePredicate(p, _appsReturnPeriod)).Sum(c => c.Amount ?? 0m);

                        // NOTE: Additional earigns calc required for this field
                        reportRecord.CoInvestmentOutstandingFromEmplToDate = paymentValues.Where(p => PeriodCoInvestmentDueFromEmployerPaymentsTypePredicateToPeriod(p, _appsReturnPeriod)).Sum(c => c.Amount ?? 0m);

                        // Pull across the ULN if it's not already there (we can pick the first record as the set is matched on learner ref so all ULNs in it will be the same)
                        reportRecord.PaymentUniqueLearnerNumber = paymentValues.FirstOrDefault()?.LearnerUln;

                        // Extract company name
                        string employerName;
                        if ((employerNameDictionary != null) && employerNameDictionary.TryGetValue(paymentValues.FirstOrDefault().ApprenticeshipId ?? 0, out employerName))
                        {
                            reportRecord.EmployerName = employerName;
                            if ((reportRecord.LearnerEmploymentStatusEmployerId != null) && !employerNamesToIDs.ContainsKey(reportRecord.LearnerEmploymentStatusEmployerId ?? 0))
                            {
                                employerNamesToIDs.Add(new KeyValuePair <int, string>(reportRecord.LearnerEmploymentStatusEmployerId ?? 0, reportRecord.EmployerName));
                            }
                        }
                        else
                        {
                            reportRecord.EmployerName = string.Empty;
                        }
                    }

                    if (appsCoInvestmentIlrInfo != null)
                    {
                        var ilrInfo = appsCoInvestmentIlrInfo.Learners?
                                      .FirstOrDefault(l => l.LearnRefNumber.CaseInsensitiveEquals(reportRecord.PaymentLearnerReferenceNumber));

                        if (ilrInfo != null)
                        {
                            var learningDeliveries = ilrInfo.LearningDeliveries.Where(p => p.LearnRefNumber == reportRecord.PaymentLearnerReferenceNumber);
                            if ((learningDeliveries != null) && (learningDeliveries.Count() > 0))
                            {
                                foreach (var learningDelivery in learningDeliveries)
                                {
                                    IReadOnlyCollection <AppFinRecordInfo> currentYearData = learningDelivery.AppFinRecords.Where(x => x.AFinDate >= Generics.BeginningOfYear && x.AFinDate <= Generics.EndOfYear && x.AFinType.CaseInsensitiveEquals("PMR")).ToList();
                                    reportRecord.TotalCoInvestmentCollectedToDate          =
                                        currentYearData.Where(x => x.AFinCode == 1 || x.AFinCode == 2).Sum(x => x.AFinAmount) -
                                        currentYearData.Where(x => x.AFinCode == 3).Sum(x => x.AFinAmount);
                                }
                            }
                        }
                    }

                    // Work out total earnings
                    aECLearningDeliveryDictionary.TryGetValue(reportRecord.PaymentLearnerReferenceNumber, out List <AECLearningDeliveryPeriodisedValuesInfo> ldLearner);
                    aECPriceEpisodeDictionary.TryGetValue(reportRecord.PaymentLearnerReferenceNumber, out List <AECApprenticeshipPriceEpisodePeriodisedValuesInfo> peLearner);

                    reportRecord.TotalEarningsToDate = CalculatePriceEpisodeEarningsToPeriod(ldLearner, peLearner, true, _appsReturnPeriod, reportRecord) +
                                                       CalculateLearningDeliveryEarningsToPeriod(ldLearner, true, _appsReturnPeriod, reportRecord);

                    reportRecord.TotalEarningsForPeriod = CalculatePriceEpisodeEarningsToPeriod(ldLearner, peLearner, false, _appsReturnPeriod, reportRecord) +
                                                          CalculateLearningDeliveryEarningsToPeriod(ldLearner, false, _appsReturnPeriod, reportRecord);

                    // Get any missing funding line types from earnings
                    if (string.IsNullOrEmpty(reportRecord.PaymentFundingLineType) && learnerLevelDAsInfo != null && learnerLevelDAsInfo.AECPriceEpisodeFLTsInfo != null)
                    {
                        reportRecord.PaymentFundingLineType = learnerLevelDAsInfo.AECPriceEpisodeFLTsInfo.FirstOrDefault(p => p.LearnerReferenceNumber == reportRecord.PaymentLearnerReferenceNumber)?.PaymentFundingLineType;
                    }

                    // Default any null valued records
                    reportRecord.ESFAPlannedPaymentsThisPeriod           = reportRecord.ESFAPlannedPaymentsThisPeriod == null ? 0 : reportRecord.ESFAPlannedPaymentsThisPeriod;
                    reportRecord.PlannedPaymentsToYouToDate              = reportRecord.PlannedPaymentsToYouToDate == null ? 0 : reportRecord.PlannedPaymentsToYouToDate;
                    reportRecord.CoInvestmentOutstandingFromEmplToDate   = reportRecord.CoInvestmentOutstandingFromEmplToDate == null ? 0 : reportRecord.CoInvestmentOutstandingFromEmplToDate;
                    reportRecord.CoInvestmentPaymentsToCollectThisPeriod = reportRecord.CoInvestmentPaymentsToCollectThisPeriod == null ? 0 : reportRecord.CoInvestmentPaymentsToCollectThisPeriod;
                    reportRecord.TotalCoInvestmentCollectedToDate        = reportRecord.TotalCoInvestmentCollectedToDate == null ? 0 : reportRecord.TotalCoInvestmentCollectedToDate;

                    // Work out calculated fields
                    // Issues amount - how much the gap is between what the provider earnt and the payments the ESFA/Employer were planning to give them
                    // Following BR2 - if payments are => earnings, issues amount should be zero
                    if ((reportRecord.ESFAPlannedPaymentsThisPeriod + reportRecord.CoInvestmentPaymentsToCollectThisPeriod) >= reportRecord.TotalEarningsForPeriod)
                    {
                        reportRecord.IssuesAmount = 0;
                    }
                    else
                    {
                        reportRecord.IssuesAmount = (reportRecord.TotalEarningsForPeriod
                                                     - reportRecord.ESFAPlannedPaymentsThisPeriod
                                                     - reportRecord.CoInvestmentPaymentsToCollectThisPeriod) * -1;
                    }

                    // Work out what is remaining from employer by subtracting what they a have paid so far from their calculated payments.
                    reportRecord.CoInvestmentOutstandingFromEmplToDate = reportRecord.CoInvestmentOutstandingFromEmplToDate - reportRecord.TotalCoInvestmentCollectedToDate;

                    // Issues for non-payment - worked out in order of priority.
                    // Work out issues (Other) (NOTE: Do this first as lowest priority)
                    if (reportRecord.TotalEarningsForPeriod > reportRecord.CoInvestmentPaymentsToCollectThisPeriod +
                        reportRecord.ESFAPlannedPaymentsThisPeriod)
                    {
                        reportRecord.ReasonForIssues = Reports.LearnerLevelViewReport.ReasonForIssues_Other;
                    }

                    // Work out issues (HBCP)
                    if ((learnerLevelHBCPInfo != null) &&
                        learnerLevelHBCPInfo.HBCPModels.Any(p => p.LearnerReferenceNumber == reportRecord.PaymentLearnerReferenceNumber &&
                                                            p.NonPaymentReason == 0 &&
                                                            p.DeliveryPeriod == _appsReturnPeriod))
                    {
                        reportRecord.ReasonForIssues = Reports.LearnerLevelViewReport.ReasonForIssues_CompletionHoldbackPayment;
                    }

                    // Work out reason for issues (Clawback)
                    if (reportRecord.ESFAPlannedPaymentsThisPeriod < 0)
                    {
                        reportRecord.ReasonForIssues = Reports.LearnerLevelViewReport.ReasonForIssues_Clawback;
                    }

                    // NOTE: As per WI93411, a level of rounding is required before the comparisions can be performed
                    if ((reportRecord.TotalEarningsForPeriod > reportRecord.ESFAPlannedPaymentsThisPeriod +
                         reportRecord.CoInvestmentPaymentsToCollectThisPeriod) &&
                        (decimal.Round(reportRecord.TotalEarningsToDate ?? 0, 2) == decimal.Round((reportRecord.PlannedPaymentsToYouToDate ?? 0) +
                                                                                                  (reportRecord.TotalCoInvestmentCollectedToDate ?? 0) +
                                                                                                  (reportRecord.CoInvestmentOutstandingFromEmplToDate ?? 0), 2)))
                    {
                        reportRecord.ReasonForIssues = Reports.LearnerLevelViewReport.ReasonForIssues_Clawback;
                    }

                    // If the reason for issue is datalock then we need to set the rule description
                    if ((learnerLevelViewDASDataLockInfo != null) && (learnerLevelViewDASDataLockInfo.DASDataLocks != null))
                    {
                        var datalock = learnerLevelViewDASDataLockInfo.DASDataLocks
                                       .FirstOrDefault(x => x.LearnerReferenceNumber == reportRecord.PaymentLearnerReferenceNumber &&
                                                       x.DeliveryPeriod == _appsReturnPeriod);

                        // Check to see if any records returned
                        if (datalock != null)
                        {
                            // Extract data lock info
                            int datalock_rule_id = datalock.DataLockFailureId;

                            // calculate the rule description
                            string datalockValue         = Generics.DLockErrorRuleNamePrefix + datalock_rule_id.ToString("00");
                            reportRecord.ReasonForIssues = datalockValue;
                            reportRecord.RuleDescription = DataLockValidationMessages.Validations.FirstOrDefault(x => x.RuleId.CaseInsensitiveEquals(datalockValue))?.ErrorMessage;
                        }
                    }

                    // Default any null calculated records
                    reportRecord.IssuesAmount    = reportRecord.IssuesAmount == null ? 0 : reportRecord.IssuesAmount;
                    reportRecord.ReasonForIssues = reportRecord.ReasonForIssues == null ? string.Empty : reportRecord.ReasonForIssues;
                    reportRecord.CoInvestmentOutstandingFromEmplToDate = reportRecord.CoInvestmentOutstandingFromEmplToDate == null ? 0 : reportRecord.CoInvestmentOutstandingFromEmplToDate;

                    return(reportRecord);
                }).ToList();

                // Remove the zeroed results
                learnerLevelViewModelList.RemoveAll(p => p.TotalEarningsToDate == 0 && p.PlannedPaymentsToYouToDate == 0 && p.TotalCoInvestmentCollectedToDate == 0 &&
                                                    p.CoInvestmentOutstandingFromEmplToDate == 0 && p.TotalEarningsForPeriod == 0 && p.ESFAPlannedPaymentsThisPeriod == 0 &&
                                                    p.CoInvestmentPaymentsToCollectThisPeriod == 0 && p.IssuesAmount == 0);

                // Set the missing employer names
                foreach (var llvr in learnerLevelViewModelList.Where(w => w.EmployerName == string.Empty || w.EmployerName == null))
                {
                    string employerName;
                    if (employerNamesToIDs.TryGetValue(llvr.LearnerEmploymentStatusEmployerId ?? 0, out employerName))
                    {
                        llvr.EmployerName = employerName;
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError("Failed to Build Learner Level View", ex);
                throw ex;
            }

            return(learnerLevelViewModelList);
        }
Exemplo n.º 6
0
        public async Task GenerateReport()
        {
            DateTime dateTime     = DateTime.UtcNow;
            int      ukPrn        = 10036143;
            string   filename     = $"R01_10036143_10036143 Learner Level View Report {dateTime:yyyyMMdd-HHmmss}";
            int      academicYear = 2021;

            Mock <IReportServiceContext> reportServiceContextMock = new Mock <IReportServiceContext>();

            reportServiceContextMock.SetupGet(x => x.JobId).Returns(1);
            reportServiceContextMock.SetupGet(x => x.SubmissionDateTimeUtc).Returns(DateTime.UtcNow);
            reportServiceContextMock.SetupGet(x => x.Ukprn).Returns(10036143);
            reportServiceContextMock.SetupGet(x => x.ReturnPeriod).Returns(1);
            reportServiceContextMock.SetupGet(x => x.ReturnPeriodName).Returns("R01");

            Mock <ILogger>                     loggerMock                     = new Mock <ILogger>();
            Mock <ICsvFileService>             csvFileServiceMock             = new Mock <ICsvFileService>();
            Mock <IFileNameService>            fileNameServiceMock            = new Mock <IFileNameService>();
            Mock <IUYPSummaryViewDataProvider> uypSummaryViewDataProviderMock = new Mock <IUYPSummaryViewDataProvider>();
            Mock <IUYPSummaryViewModelBuilder> uypSummaryViewModelBuilderMock = new Mock <IUYPSummaryViewModelBuilder>();
            Mock <IJsonSerializationService>   jsonSerializationServiceMock   = new Mock <IJsonSerializationService>();
            IJsonSerializationService          jsonSerializationService       = new JsonSerializationService();
            Mock <IFileService>                fileServiceMock                = new Mock <IFileService>();
            Mock <IReportDataPersistanceService <LearnerLevelViewReport> > reportDataPersistanceServiceMock =
                new Mock <IReportDataPersistanceService <LearnerLevelViewReport> >();
            Mock <IUYPSummaryViewPersistenceMapper> uypSummaryViewPersistenceMapper = new Mock <IUYPSummaryViewPersistenceMapper>();

            var summaryReportDataPersistanceServiceMock = new Mock <IReportDataPersistanceService <UYPSummaryViewReport> >();
            var zipServiceMock = new Mock <IReportZipService>();

            uypSummaryViewPersistenceMapper.Setup(x => x.Map(
                                                      It.IsAny <IReportServiceContext>(),
                                                      It.IsAny <IEnumerable <LearnerLevelViewModel> >(),
                                                      It.IsAny <CancellationToken>())).Returns(new List <LearnerLevelViewReport>());

            uypSummaryViewPersistenceMapper.Setup(x => x.Map(
                                                      It.IsAny <IReportServiceContext>(),
                                                      It.IsAny <IEnumerable <LearnerLevelViewSummaryModel> >())).Returns(new List <UYPSummaryViewReport>());

            reportDataPersistanceServiceMock.Setup(x => x.PersistAsync(
                                                       It.IsAny <IReportServiceContext>(),
                                                       It.IsAny <IEnumerable <LearnerLevelViewReport> >(),
                                                       It.IsAny <CancellationToken>()));

            summaryReportDataPersistanceServiceMock.Setup(x => x.PersistAsync(
                                                              It.IsAny <IReportServiceContext>(),
                                                              It.IsAny <IEnumerable <UYPSummaryViewReport> >(),
                                                              It.IsAny <CancellationToken>()));

            jsonSerializationServiceMock.Setup(x => x.Serialize <IEnumerable <LearnerLevelViewSummaryModel> >(
                                                   It.IsAny <IEnumerable <LearnerLevelViewSummaryModel> >())).Returns(string.Empty);

            // We need three streams
            FileStream[] fs = new FileStream[3] {
                new FileStream("1.json", FileMode.Create), new FileStream("2.csv", FileMode.Create), new FileStream("3.csv", FileMode.Create)
            };
            var index = 0;

            fileServiceMock.Setup(x => x.OpenWriteStreamAsync(
                                      It.IsAny <string>(),
                                      It.IsAny <string>(),
                                      It.IsAny <CancellationToken>())).ReturnsAsync(() => fs[index++]);

            // Return filenames
            fileNameServiceMock.Setup(x => x.GetFilename(It.IsAny <IReportServiceContext>(), It.IsAny <string>(), It.IsAny <OutputTypes>(), It.IsAny <bool>(), It.IsAny <bool>())).Returns(filename);

            // Setup data return objects
            ICollection <CoInvestmentInfo> coInvestmentInfo = new CoInvestmentInfo[1] {
                BuildILRCoInvestModel(ukPrn)
            };

            uypSummaryViewDataProviderMock.Setup(x => x.GetCoinvestmentsAsync(It.IsAny <int>(), It.IsAny <CancellationToken>())).ReturnsAsync(coInvestmentInfo);

            ICollection <Payment> payments = BuildDasPaymentsModel(ukPrn, academicYear);

            uypSummaryViewDataProviderMock.Setup(x => x.GetDASPaymentsAsync(It.IsAny <int>(), It.IsAny <int>(), It.IsAny <CancellationToken>())).ReturnsAsync(payments);

            ICollection <DataLock> dataLocks = new DataLock[1] {
                new DataLock()
                {
                    DataLockFailureId = 1, CollectionPeriod = 1, LearnerReferenceNumber = "A12345"
                }
            };

            uypSummaryViewDataProviderMock.Setup(x => x.GetDASDataLockAsync(It.IsAny <int>(), It.IsAny <int>(), It.IsAny <CancellationToken>())).ReturnsAsync(dataLocks);

            ICollection <HBCPInfo> hbcpInfo = new HBCPInfo[1] {
                new HBCPInfo()
                {
                    LearnerReferenceNumber = "A12345", CollectionPeriod = 1, NonPaymentReason = 1
                }
            };

            uypSummaryViewDataProviderMock.Setup(x => x.GetHBCPInfoAsync(It.IsAny <int>(), It.IsAny <int>(), It.IsAny <CancellationToken>())).ReturnsAsync(hbcpInfo);

            ICollection <Learner> learners = BuildILRModel(ukPrn);

            uypSummaryViewDataProviderMock.Setup(x => x.GetILRLearnerInfoAsync(It.IsAny <int>(), It.IsAny <CancellationToken>())).ReturnsAsync(learners);

            ICollection <LearningDeliveryEarning> ldEarnings = BuildLDEarningsModel(ukPrn);

            uypSummaryViewDataProviderMock.Setup(x => x.GetLearnerDeliveryEarningsAsync(It.IsAny <int>(), It.IsAny <CancellationToken>())).ReturnsAsync(ldEarnings);

            IDictionary <long, string> legalEntityNames = BuildLegalEntityNames();

            uypSummaryViewDataProviderMock.Setup(x => x.GetLegalEntityNameAsync(It.IsAny <int>(), It.IsAny <IEnumerable <long> >(), It.IsAny <CancellationToken>())).ReturnsAsync(legalEntityNames);

            ICollection <PriceEpisodeEarning> peEarnings = BuildPEEarningsModel(ukPrn);

            uypSummaryViewDataProviderMock.Setup(x => x.GetPriceEpisodeEarningsAsync(It.IsAny <int>(), It.IsAny <CancellationToken>())).ReturnsAsync(peEarnings);

            ICollection <LearnerLevelViewModel> results = new LearnerLevelViewModel[1] {
                new LearnerLevelViewModel()
                {
                    Ukprn = ukPrn,
                    PaymentLearnerReferenceNumber = "A12345",
                    PaymentUniqueLearnerNumbers   = "12345",
                    FamilyName = "Banner",
                    GivenNames = "Bruce",
                    LearnerEmploymentStatusEmployerId = 1,
                    EmployerName                            = "Employer Name",
                    TotalEarningsToDate                     = 1m,
                    PlannedPaymentsToYouToDate              = 2m,
                    TotalCoInvestmentCollectedToDate        = 3m,
                    CoInvestmentOutstandingFromEmplToDate   = 4m,
                    TotalEarningsForPeriod                  = 5m,
                    ESFAPlannedPaymentsThisPeriod           = 6m,
                    CoInvestmentPaymentsToCollectThisPeriod = 7m,
                    IssuesAmount                            = 8m,
                    ReasonForIssues                         = "Borked",
                    PaymentFundingLineType                  = "12345",
                    RuleDescription                         = "Rule X"
                }
            };

            uypSummaryViewModelBuilderMock.Setup(x => x.Build(
                                                     It.IsAny <ICollection <Payment> >(),
                                                     It.IsAny <ICollection <Learner> >(),
                                                     It.IsAny <ICollection <LearningDeliveryEarning> >(),
                                                     It.IsAny <ICollection <PriceEpisodeEarning> >(),
                                                     It.IsAny <ICollection <CoInvestmentInfo> >(),
                                                     It.IsAny <ICollection <DataLock> >(),
                                                     It.IsAny <ICollection <HBCPInfo> >(),
                                                     It.IsAny <IDictionary <long, string> >(),
                                                     It.IsAny <int>(),
                                                     It.IsAny <int>())).Returns(results);

            // Create and invoke the view
            Reports.UYPSummaryView.UYPSummaryView report
                = new Reports.UYPSummaryView.UYPSummaryView(
                      csvFileServiceMock.Object,
                      fileNameServiceMock.Object,
                      uypSummaryViewDataProviderMock.Object,
                      uypSummaryViewModelBuilderMock.Object,
                      jsonSerializationService,
                      fileServiceMock.Object,
                      zipServiceMock.Object,
                      reportDataPersistanceServiceMock.Object,
                      summaryReportDataPersistanceServiceMock.Object,
                      uypSummaryViewPersistenceMapper.Object,
                      loggerMock.Object);
            await report.GenerateReport(reportServiceContextMock.Object, CancellationToken.None);

            List <LearnerLevelViewSummaryModel> result;

            using (var reader = new StreamReader(fs[0].Name))
            {
                string fileData = reader.ReadToEnd();
                result = jsonSerializationService.Deserialize <IEnumerable <LearnerLevelViewSummaryModel> >(fileData).ToList();
            }

            result.Should().NotBeNullOrEmpty();
            result.Count().Should().Be(1);

            result[0].CoInvestmentPaymentsToCollectForThisPeriod.Should().Be(7);
            result[0].ESFAPlannedPaymentsForThisPeriod.Should().Be(6);
            result[0].NumberofLearners.Should().Be(1);
            result[0].TotalCoInvestmentCollectedToDate.Should().Be(3);
            result[0].NumberofClawbacks.Should().Be(0);
        }
        public ICollection <LearnerLevelViewModel> Build(
            ICollection <Payment> payments,
            ICollection <Learner> learners,
            ICollection <LearningDeliveryEarning> ldEarnings,
            ICollection <PriceEpisodeEarning> peEarnings,
            ICollection <CoInvestmentInfo> coInvestmentInfo,
            ICollection <DataLock> datalocks,
            ICollection <HBCPInfo> hbcpInfo,
            IDictionary <long, string> legalEntityNameDictionary,
            int returnPeriod,
            int ukprn)
        {
            // this variable is the final report and is the return value of this method.
            List <LearnerLevelViewModel> learnerLevelViewModelList = null;

            try
            {
                // Lookup for employer IDs to employer name
                IDictionary <int, string> employerNamesToIDs = new Dictionary <int, string>();

                // Build dictionaries to allow quicker processing of the larger datasets
                var paymentsDictionary = payments.GroupBy(e => e.LearnerReferenceNumber, StringComparer.OrdinalIgnoreCase)
                                         .ToDictionary(e => e.Key, e => e.ToList(), StringComparer.OrdinalIgnoreCase);

                var ilrLearnersDict = learners.ToDictionary(t => t.LearnRefNumber, StringComparer.OrdinalIgnoreCase);

                var newLdEarningsDictionary = ldEarnings.GroupBy(e => e.LearnRefNumber, StringComparer.OrdinalIgnoreCase)
                                              .ToDictionary(e => e.Key, e => e.ToList(), StringComparer.OrdinalIgnoreCase);

                var newPeEarningsDictionary = peEarnings.GroupBy(e => e.LearnRefNumber, StringComparer.OrdinalIgnoreCase)
                                              .ToDictionary(e => e.Key, e => e.ToList(), StringComparer.OrdinalIgnoreCase);

                var dataLockHashset = datalocks.ToImmutableHashSet(_dataLockComparer);

                // Union the keys from the datasets being used to source the report
                var unionedKeys = UnionKeys(payments, ldEarnings, peEarnings);

                // Populate the learner list using the ILR query first
                learnerLevelViewModelList = unionedKeys
                                            .Select(record =>
                {
                    var reportRecord = new LearnerLevelViewModel()
                    {
                        Ukprn = ukprn,
                        PaymentLearnerReferenceNumber = record
                    };

                    // Extract ILR info
                    if (ilrLearnersDict.TryGetValue(record, out var ilrRecord))
                    {
                        reportRecord.FamilyName = ilrRecord.FamilyName;
                        reportRecord.GivenNames = ilrRecord.GivenNames;
                        reportRecord.PaymentUniqueLearnerNumbers = ilrRecord.ULN.ToString();
                        if ((ilrRecord.LearnerEmploymentStatuses != null) && (ilrRecord.LearnerEmploymentStatuses.Count > 0))
                        {
                            reportRecord.LearnerEmploymentStatusEmployerId = ilrRecord.LearnerEmploymentStatuses.Where(les => les.LearnRefNumber.CaseInsensitiveEquals(reportRecord.PaymentLearnerReferenceNumber) &&
                                                                                                                       les?.EmpStat == 10)
                                                                             .OrderByDescending(les => les?.DateEmpStatApp)
                                                                             .FirstOrDefault()?.EmpId;
                        }
                    }

                    if (paymentsDictionary.TryGetValue(reportRecord.PaymentLearnerReferenceNumber, out var paymentValues))
                    {
                        // Assign the amounts
                        reportRecord.PlannedPaymentsToYouToDate = paymentValues.Where(p => p.CollectionPeriod <= returnPeriod &&
                                                                                      (TotalESFAPlannedPaymentFSTypePredicate(p) ||
                                                                                       TotalESFAPlannedPaymentsTTTypePredicate(p) ||
                                                                                       TotalESFAPlannedPaymentsNonZPTypePredicate(p))).Sum(c => c.Amount);

                        reportRecord.ESFAPlannedPaymentsThisPeriod = paymentValues.Where(p => p.CollectionPeriod == returnPeriod &&
                                                                                         (TotalESFAPlannedPaymentFSTypePredicate(p) ||
                                                                                          TotalESFAPlannedPaymentsTTTypePredicate(p) ||
                                                                                          TotalESFAPlannedPaymentsNonZPTypePredicate(p))).Sum(c => c.Amount);

                        reportRecord.CoInvestmentPaymentsToCollectThisPeriod = paymentValues.Where(p => p.CollectionPeriod == returnPeriod &&
                                                                                                   TotalCoInvestmentPaymentsDueFromEmployerTypePredicate(p)).Sum(c => c.Amount);

                        // NOTE: Additional earnings calc required for this field
                        reportRecord.CoInvestmentOutstandingFromEmplToDate = paymentValues.Where(p => p.CollectionPeriod <= returnPeriod &&
                                                                                                 TotalCoInvestmentPaymentsDueFromEmployerTypePredicate(p)).Sum(c => c.Amount);

                        // Add any further ULNs found
                        var otherULNs    = paymentValues.Select(x => x.LearnerUln.ToString()).Distinct().ToList();
                        var existingULNS = reportRecord.PaymentUniqueLearnerNumbers.Split(';');
                        foreach (string ULN in otherULNs)
                        {
                            if (!existingULNS.Contains(ULN))
                            {
                                reportRecord.PaymentUniqueLearnerNumbers = (reportRecord.PaymentUniqueLearnerNumbers == null ? ULN: reportRecord.PaymentUniqueLearnerNumbers + ";" + ULN);
                            }
                        }

                        // Extract company name
                        if ((legalEntityNameDictionary != null) && legalEntityNameDictionary.TryGetValue(paymentValues.First().ApprenticeshipId, out var employerName))
                        {
                            reportRecord.EmployerName = employerName;
                            if ((reportRecord.LearnerEmploymentStatusEmployerId != null) && !employerNamesToIDs.ContainsKey(reportRecord.LearnerEmploymentStatusEmployerId ?? 0))
                            {
                                employerNamesToIDs.Add(new KeyValuePair <int, string>(reportRecord.LearnerEmploymentStatusEmployerId ?? 0, reportRecord.EmployerName));
                            }
                        }
                        else
                        {
                            reportRecord.EmployerName = string.Empty;
                        }
                    }

                    // Extract coInvestmentInfo - NOTE: is possible to have duplicate LearnerRef numbers in this list
                    if (coInvestmentInfo != null)
                    {
                        var learningDeliveries = coInvestmentInfo.Where(p => p.LearnRefNumber == reportRecord.PaymentLearnerReferenceNumber).ToList();

                        if (learningDeliveries.Any())
                        {
                            reportRecord.TotalCoInvestmentCollectedToDate =
                                learningDeliveries.Where(x => x.AFinCode == 1 || x.AFinCode == 2).Sum(x => x.AFinAmount) -
                                learningDeliveries.Where(x => x.AFinCode == 3).Sum(x => x.AFinAmount);
                        }
                    }

                    // Work out total earnings
                    var ldLearner = newLdEarningsDictionary.GetValueOrDefault(reportRecord.PaymentLearnerReferenceNumber, Enumerable.Empty <LearningDeliveryEarning>().ToList());
                    var peLearner = newPeEarningsDictionary.GetValueOrDefault(reportRecord.PaymentLearnerReferenceNumber, Enumerable.Empty <PriceEpisodeEarning>().ToList());

                    reportRecord.TotalEarningsToDate = CalculatePriceEpisodeEarningsToPeriod(ldLearner, peLearner, true, returnPeriod, reportRecord) +
                                                       CalculateLearningDeliveryEarningsToPeriod(ldLearner, true, returnPeriod, reportRecord);

                    reportRecord.TotalEarningsForPeriod = CalculatePriceEpisodeEarningsToPeriod(ldLearner, peLearner, false, returnPeriod, reportRecord) +
                                                          CalculateLearningDeliveryEarningsToPeriod(ldLearner, false, returnPeriod, reportRecord);

                    // Default any null valued records
                    reportRecord.ESFAPlannedPaymentsThisPeriod           = reportRecord.ESFAPlannedPaymentsThisPeriod ?? 0;
                    reportRecord.PlannedPaymentsToYouToDate              = reportRecord.PlannedPaymentsToYouToDate ?? 0;
                    reportRecord.CoInvestmentOutstandingFromEmplToDate   = reportRecord.CoInvestmentOutstandingFromEmplToDate ?? 0;
                    reportRecord.CoInvestmentPaymentsToCollectThisPeriod = reportRecord.CoInvestmentPaymentsToCollectThisPeriod ?? 0;
                    reportRecord.TotalCoInvestmentCollectedToDate        = reportRecord.TotalCoInvestmentCollectedToDate ?? 0;

                    // Work out calculated fields
                    // Issues amount - how much the gap is between what the provider earnt and the payments the ESFA/Employer were planning to give them
                    // Following BR2 - if payments are => earnings, issues amount should be zero
                    if ((reportRecord.ESFAPlannedPaymentsThisPeriod + reportRecord.CoInvestmentPaymentsToCollectThisPeriod) >= reportRecord.TotalEarningsForPeriod)
                    {
                        reportRecord.IssuesAmount = 0;
                    }
                    else
                    {
                        reportRecord.IssuesAmount = (decimal.Round((reportRecord.TotalEarningsForPeriod ?? 0)
                                                                   - (reportRecord.CoInvestmentPaymentsToCollectThisPeriod ?? 0)
                                                                   - (reportRecord.ESFAPlannedPaymentsThisPeriod ?? 0), LearnerLevelViewConstants.LLVRoundingAccuracy)) * -1;
                    }

                    // Work out what is remaining from employer by subtracting what they a have paid so far from their calculated payments.
                    reportRecord.CoInvestmentOutstandingFromEmplToDate = reportRecord.CoInvestmentOutstandingFromEmplToDate - reportRecord.TotalCoInvestmentCollectedToDate;

                    // Issues for non-payment - worked out in order of priority.
                    // Work out issues (Other) (NOTE: Do this first as lowest priority)
                    // Include rounding in calculation
                    if (reportRecord.IssuesAmount != 0)
                    {
                        reportRecord.ReasonForIssues = LearnerLevelViewConstants.ReasonForIssues_Other;
                    }

                    // Work out issues (HBCP)
                    if ((hbcpInfo != null) &&
                        hbcpInfo.Any(p => p.LearnerReferenceNumber == reportRecord.PaymentLearnerReferenceNumber &&
                                     p.NonPaymentReason != null &&
                                     p.CollectionPeriod == returnPeriod))
                    {
                        reportRecord.ReasonForIssues = LearnerLevelViewConstants.ReasonForIssues_CompletionHoldbackPayment;
                    }

                    // Work out reason for issues (Clawback)
                    if (reportRecord.ESFAPlannedPaymentsThisPeriod < 0)
                    {
                        reportRecord.ReasonForIssues = LearnerLevelViewConstants.ReasonForIssues_Clawback;
                    }

                    // NOTE: As per WI93411, a level of rounding is required before the comparisons can be performed
                    if ((reportRecord.IssuesAmount != 0) &&
                        (decimal.Round((reportRecord.TotalEarningsToDate ?? 0)
                                       - (reportRecord.PlannedPaymentsToYouToDate ?? 0)
                                       - (reportRecord.TotalCoInvestmentCollectedToDate ?? 0)
                                       - (reportRecord.CoInvestmentOutstandingFromEmplToDate ?? 0), LearnerLevelViewConstants.LLVRoundingAccuracy) == 0))
                    {
                        reportRecord.ReasonForIssues = LearnerLevelViewConstants.ReasonForIssues_Clawback;
                    }

                    // If the reason for issue is datalock then we need to set the rule description
                    if ((datalocks != null) && (datalocks.Any()))
                    {
                        var datalock = dataLockHashset.FirstOrDefault(x => x.LearnerReferenceNumber == reportRecord.PaymentLearnerReferenceNumber &&
                                                                      x.CollectionPeriod == returnPeriod);

                        // Check to see if any records returned
                        if (datalock != null)
                        {
                            // Extract data lock info
                            int datalockRuleId = datalock.DataLockFailureId;

                            // calculate the rule description
                            string datalockValue         = LearnerLevelViewConstants.DLockErrorRuleNamePrefix + datalockRuleId.ToString("00");
                            reportRecord.ReasonForIssues = datalockValue;
                            reportRecord.RuleDescription = DataLockValidationMessages.Validations.FirstOrDefault(x => x.RuleId.CaseInsensitiveEquals(datalockValue))?.ErrorMessage;
                        }
                    }

                    // Default any null calculated records
                    reportRecord.IssuesAmount    = reportRecord.IssuesAmount ?? 0;
                    reportRecord.ReasonForIssues = reportRecord.ReasonForIssues ?? string.Empty;
                    reportRecord.CoInvestmentOutstandingFromEmplToDate = reportRecord.CoInvestmentOutstandingFromEmplToDate ?? 0;

                    return(reportRecord);
                }).OrderBy(o => o.PaymentLearnerReferenceNumber)
                                            .ThenBy(o => o.PaymentFundingLineType).ToList();

                // Remove the zeroed results - checked documentation and performance of this should be similar to a .Where
                learnerLevelViewModelList.RemoveAll(p => p.TotalEarningsToDate == 0 && p.PlannedPaymentsToYouToDate == 0 && p.TotalCoInvestmentCollectedToDate == 0 &&
                                                    p.CoInvestmentOutstandingFromEmplToDate == 0 && p.TotalEarningsForPeriod == 0 && p.ESFAPlannedPaymentsThisPeriod == 0 &&
                                                    p.CoInvestmentPaymentsToCollectThisPeriod == 0 && p.IssuesAmount == 0);

                // Set the missing employer names
                foreach (var llvr in learnerLevelViewModelList.Where(w => String.IsNullOrEmpty(w.EmployerName)))
                {
                    if (employerNamesToIDs.TryGetValue(llvr.LearnerEmploymentStatusEmployerId ?? 0, out var employerName))
                    {
                        llvr.EmployerName = employerName;
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError("Failed to Build Learner Level View", ex);
                throw ex;
            }

            return(learnerLevelViewModelList);
        }