public void CalculateProviderResult_WhenCalculationsAreNotEmpty_ShouldReturnCorrectResult() { // Arrange List <Reference> policySpecificationsForFundingCalc = new List <Reference>() { new Reference("Spec1", "SpecOne"), new Reference("Spec2", "SpecTwo") }; List <Reference> policySpecificationsForNumberCalc = new List <Reference>() { new Reference("Spec1", "SpecOne"), }; Reference allocationLineReturned = new Reference("allocationLine", "allocation line for Funding Calc and number calc"); Reference fundingCalcReference = new Reference("CalcF1", "Funding calc 1"); Reference fundingCalcSpecificationReference = new Reference("FSpect", "FundingSpecification"); Reference numbercalcReference = new Reference("CalcF2", "Funding calc 2"); Reference numbercalcSpecificationReference = new Reference("FSpec2", "FundingSpecification2"); CalculationResult fundingCalcReturned = new CalculationResult() { CalculationType = CalculationType.Funding, Calculation = fundingCalcReference, AllocationLine = allocationLineReturned, CalculationSpecification = fundingCalcSpecificationReference, PolicySpecifications = policySpecificationsForFundingCalc, Value = 10000 }; CalculationResult fundingCalcReturned2 = new CalculationResult() { CalculationType = CalculationType.Funding, Calculation = numbercalcReference, AllocationLine = allocationLineReturned, CalculationSpecification = numbercalcSpecificationReference, PolicySpecifications = policySpecificationsForNumberCalc, Value = 20000 }; List <CalculationResult> calculationResults = new List <CalculationResult>() { fundingCalcReturned, fundingCalcReturned2 }; IAllocationModel mockAllocationModel = Substitute.For <IAllocationModel>(); mockAllocationModel .Execute(Arg.Any <List <ProviderSourceDataset> >(), Arg.Any <ProviderSummary>()) .Returns(calculationResults); CalculationEngine calculationEngine = CreateCalculationEngine(); ProviderSummary providerSummary = CreateDummyProviderSummary(); BuildProject buildProject = CreateBuildProject(); var nonMatchingCalculationModel = new CalculationSummaryModel() { Id = "Non matching calculation", Name = "Non matching calculation", CalculationType = CalculationType.Funding }; IEnumerable <CalculationSummaryModel> calculationSummaryModels = new[] { new CalculationSummaryModel() { Id = fundingCalcReference.Id, Name = fundingCalcReference.Name, CalculationType = CalculationType.Funding }, new CalculationSummaryModel() { Id = numbercalcReference.Id, Name = numbercalcReference.Name, CalculationType = CalculationType.Funding }, nonMatchingCalculationModel }; // Act var calculateProviderResults = calculationEngine.CalculateProviderResults(mockAllocationModel, buildProject, calculationSummaryModels, providerSummary, new List <ProviderSourceDataset>()); ProviderResult result = calculateProviderResults; // Assert result.Provider.Should().Be(providerSummary); result.SpecificationId.Should().BeEquivalentTo(buildProject.SpecificationId); result.Id.Should().BeEquivalentTo(GenerateId(providerSummary.Id, buildProject.SpecificationId)); result.CalculationResults.Should().HaveCount(3); result.AllocationLineResults.Should().HaveCount(1); AllocationLineResult allocationLine = result.AllocationLineResults[0]; allocationLine.Value = 30000; CalculationResult fundingCalcResult = result.CalculationResults.First(cr => cr.Calculation.Id == fundingCalcReference.Id); fundingCalcResult.Calculation.Should().BeEquivalentTo(fundingCalcReference); fundingCalcResult.CalculationType.Should().BeEquivalentTo(fundingCalcReturned.CalculationType); fundingCalcResult.AllocationLine.Should().BeEquivalentTo(allocationLineReturned); fundingCalcResult.CalculationSpecification.Should().BeEquivalentTo(fundingCalcSpecificationReference); fundingCalcResult.PolicySpecifications.Should().BeEquivalentTo(policySpecificationsForFundingCalc); fundingCalcResult.Value.Should().Be(fundingCalcReturned.Value.Value); CalculationResult numberCalcResult = result.CalculationResults.First(cr => cr.Calculation.Id == numbercalcReference.Id); numberCalcResult.Calculation.Should().BeEquivalentTo(numbercalcReference); numberCalcResult.CalculationType.Should().BeEquivalentTo(fundingCalcReturned2.CalculationType); numberCalcResult.AllocationLine.Should().BeEquivalentTo(allocationLineReturned); numberCalcResult.CalculationSpecification.Should().BeEquivalentTo(numbercalcSpecificationReference); numberCalcResult.PolicySpecifications.Should().BeEquivalentTo(policySpecificationsForNumberCalc); numberCalcResult.Value.Should().Be(fundingCalcReturned2.Value.Value); CalculationResult nonMatchingCalcResult = result.CalculationResults.First(cr => cr.Calculation.Id == "Non matching calculation"); nonMatchingCalcResult.Calculation.Should().BeEquivalentTo(new Reference(nonMatchingCalculationModel.Id, nonMatchingCalculationModel.Name)); nonMatchingCalcResult.CalculationType.Should().BeEquivalentTo(nonMatchingCalculationModel.CalculationType); nonMatchingCalcResult.AllocationLine.Should().BeNull(); nonMatchingCalcResult.CalculationSpecification.Should().BeNull(); nonMatchingCalcResult.PolicySpecifications.Should().BeNull(); nonMatchingCalcResult.Value.Should().BeNull(); }
private async Task <IActionResult> GenerateAndCompile(BuildProject buildProject, Calculation calculationToPreview, IEnumerable <Calculation> calculations, CompilerOptions compilerOptions, PreviewRequest previewRequest) { PreviewProviderCalculationResponseModel previewProviderCalculation = null; Build compilerOutput = _sourceCodeService.Compile(buildProject, calculations, compilerOptions); if (compilerOutput.SourceFiles != null) { await _sourceCodeService.SaveSourceFiles(compilerOutput.SourceFiles, buildProject.SpecificationId, SourceCodeType.Preview); } if (compilerOutput.Success) { _logger.Information($"Build compiled successfully for calculation id {calculationToPreview.Id}"); string calculationIdentifier = $"{_typeIdentifierGenerator.GenerateIdentifier(calculationToPreview.Namespace)}.{_typeIdentifierGenerator.GenerateIdentifier(calculationToPreview.Name)}"; IDictionary <string, string> functions = _sourceCodeService.GetCalculationFunctions(compilerOutput.SourceFiles); IDictionary <string, string> calculationIdentifierMap = calculations .Select(_ => new { Identifier = $"{_typeIdentifierGenerator.GenerateIdentifier(_.Namespace)}.{_typeIdentifierGenerator.GenerateIdentifier(_.Name)}", CalcName = _.Name }) .ToDictionary(d => d.Identifier, d => d.CalcName); if (!functions.ContainsKey(calculationIdentifier)) { compilerOutput.Success = false; compilerOutput.CompilerMessages.Add(new CompilerMessage { Message = $"{calculationIdentifier} is not an aggregable field", Severity = Severity.Error }); } else { if (previewRequest != null) { if (!SourceCodeHelpers.HasReturn(previewRequest.SourceCode)) { compilerOutput.Success = false; compilerOutput.CompilerMessages.Add(new CompilerMessage { Message = $"{calculationIdentifier} must have a return statement so that a calculation result will be returned", Severity = Severity.Error }); } else { IEnumerable <string> aggregateParameters = SourceCodeHelpers.GetCalculationAggregateFunctionParameters(previewRequest.SourceCode); bool continueChecking = true; if (!aggregateParameters.IsNullOrEmpty()) { foreach (string aggregateParameter in aggregateParameters) { if (!functions.ContainsKey(aggregateParameter)) { compilerOutput.Success = false; compilerOutput.CompilerMessages.Add(new CompilerMessage { Message = $"{aggregateParameter} is not an aggregable field", Severity = Severity.Error }); continueChecking = false; break; } if (calculationIdentifierMap.ContainsKey(aggregateParameter)) { Calculation calculation = calculations.SingleOrDefault(_ => _.Name == calculationIdentifierMap[aggregateParameter]); if (calculation.Current.DataType != CalculationDataType.Decimal) { compilerOutput.Success = false; compilerOutput.CompilerMessages.Add(new CompilerMessage { Message = $"Only decimal fields can be used on aggregation. {aggregateParameter} has data type of {calculation.Current.DataType}", Severity = Severity.Error }); continueChecking = false; break; } } } if (continueChecking) { if (SourceCodeHelpers.IsCalcReferencedInAnAggregate(functions, calculationIdentifier)) { compilerOutput.Success = false; compilerOutput.CompilerMessages.Add(new CompilerMessage { Message = $"{calculationIdentifier} is already referenced in an aggregation that would cause nesting", Severity = Severity.Error }); } else if (SourceCodeHelpers.CheckSourceForExistingCalculationAggregates(functions, previewRequest.SourceCode)) { compilerOutput.Success = false; compilerOutput.CompilerMessages.Add(new CompilerMessage { Message = $"{calculationIdentifier} cannot reference another calc that is being aggregated", Severity = Severity.Error }); } } } } } } } else { _logger.Information($"Build did not compile successfully for calculation id {calculationToPreview.Id}"); } LogMessages(compilerOutput, buildProject, calculationToPreview); if (!string.IsNullOrEmpty(previewRequest.ProviderId)) { CalculationSummaryModel calculationSummaryModel = calculationToPreview.ToSummaryModel(); CalcEngineModels.CalculationSummaryModel model = _mapper.Map <CalcEngineModels.CalculationSummaryModel>(calculationSummaryModel); CalcEngineModels.PreviewCalculationRequest previewCalculationRequest = new CalcEngineModels.PreviewCalculationRequest { AssemblyContent = compilerOutput.Assembly, PreviewCalculationSummaryModel = model }; ApiResponse <CalcEngineProviderResult> previewCalcResultApiResponse = await _calcEngineApiClientPolicy.ExecuteAsync( () => _calcEngineApiClient.PreviewCalculationResults( previewRequest.SpecificationId, previewRequest.ProviderId, previewCalculationRequest)); if (previewCalcResultApiResponse.StatusCode.IsSuccess()) { CalcEngineProviderResult calcEngineProviderResult = previewCalcResultApiResponse.Content; previewProviderCalculation = new PreviewProviderCalculationResponseModel { ProviderName = calcEngineProviderResult.Provider.Name, CalculationResult = _mapper.Map <CalculationResult>( calcEngineProviderResult.CalculationResults.SingleOrDefault(_ => _.Calculation?.Id == calculationToPreview.Id)), }; } } return(new OkObjectResult(new PreviewResponse { Calculation = calculationToPreview.ToResponseModel(), CompilerOutput = compilerOutput, PreviewProviderCalculation = previewProviderCalculation })); }
public ProviderResult CalculateProviderResults( IAllocationModel model, string specificationId, IEnumerable <CalculationSummaryModel> calculations, ProviderSummary provider, IDictionary <string, ProviderSourceDataset> providerSourceDatasets, IEnumerable <CalculationAggregation> aggregations = null) { var stopwatch = new Stopwatch(); stopwatch.Start(); CalculationResultContainer calculationResultContainer = model.Execute(providerSourceDatasets, provider, aggregations); IEnumerable <CalculationResult> calculationResultItems = calculationResultContainer.CalculationResults; stopwatch.Stop(); IDictionary <string, double> metrics = new Dictionary <string, double>() { { "calculation-provider-calcsMs", stopwatch.ElapsedMilliseconds }, { "calculation-provider-calcsTotal", calculations.AnyWithNullCheck() ? calculations.Count() : 0 }, { "calculation-provider-exceptions", calculationResultItems.AnyWithNullCheck() ? calculationResultItems.Count(c => !string.IsNullOrWhiteSpace(c.ExceptionMessage)) : 0 }, }; _telemetry.TrackEvent("CalculationRunProvider", new Dictionary <string, string>() { { "specificationId", specificationId }, }, metrics ); if (calculationResultItems.AnyWithNullCheck() && calculationResultItems.Count() > 0) { _logger.Verbose($"Processed results for {calculationResultItems.Count()} calcs in {stopwatch.ElapsedMilliseconds}ms ({stopwatch.ElapsedMilliseconds / calculationResultItems.Count(): 0.0000}ms)"); } else { _logger.Information("There are no calculations to executed for specification ID {specificationId}", specificationId); } byte[] plainTextBytes = System.Text.Encoding.UTF8.GetBytes($"{provider.Id}-{specificationId}"); ProviderResult providerResult = new ProviderResult { Id = Convert.ToBase64String(plainTextBytes), Provider = provider, SpecificationId = specificationId }; if (calculationResultItems.AnyWithNullCheck()) { foreach (CalculationResult calcResult in calculationResultItems) { CalculationSummaryModel calculationSummaryModel = calculations.First(c => c.Id == calcResult.Calculation.Id); calcResult.CalculationType = calculationSummaryModel.CalculationType; calcResult.CalculationDataType = calculationSummaryModel.CalculationValueType.ToCalculationDataType(); if (calcResult.CalculationDataType == CalculationDataType.Decimal && Decimal.Equals(decimal.MinValue, calcResult.Value)) { // The default for the calculation is to return Decimal.MinValue - if this is the case, then subsitute a 0 value as the result, instead of the negative number. calcResult.Value = 0; } } } //we need a stable sort of results to enable the cache checks by overall SHA hash on the results json providerResult.CalculationResults = calculationResultContainer.CalculationResults?.OrderBy(_ => _.Calculation.Id).ToList(); providerResult.FundingLineResults = calculationResultContainer.FundingLineResults.OrderBy(_ => _.FundingLine.Id).ToList(); return(providerResult); }
public async Task PreviewCalculationResult_GivenCachedAggregateValuesExist_CalculateProviderResultsCallReceived() { IAllocationModel allocationModel = Substitute.For <IAllocationModel>(); _calculationEngine .GenerateAllocationModel(Arg.Any <Assembly>()) .Returns(allocationModel); ProviderVersionSearchResult providerVersionSearchResult = new ProviderVersionSearchResult { UKPRN = providerId }; IEnumerable <string> dataDefinitionRelationshipIds = new List <string>(); SpecificationSummary specificationSummary = new SpecificationSummary { DataDefinitionRelationshipIds = dataDefinitionRelationshipIds, ProviderVersionId = providerVersionId }; _specificationsApiClient .GetSpecificationSummaryById(Arg.Is(specificationId)) .Returns(new ApiResponse <SpecificationSummary>(HttpStatusCode.OK, specificationSummary)); _providersApiClient .GetProviderByIdFromProviderVersion(Arg.Is(providerVersionId), Arg.Is(providerId)) .Returns(new ApiResponse <ProviderVersionSearchResult>(HttpStatusCode.OK, providerVersionSearchResult)); CalculationSummaryModel previewCalculationSummaryModel = new CalculationSummaryModel(); IEnumerable <CalculationSummaryModel> calculationSummaryModels = new List <CalculationSummaryModel> { new CalculationSummaryModel(), new CalculationSummaryModel() }; List <CalculationSummaryModel> expectedCalculationSummaryModels = calculationSummaryModels.ToList(); expectedCalculationSummaryModels.Add(previewCalculationSummaryModel); _calculationsRepository .GetCalculationSummariesForSpecification(Arg.Is(specificationId)) .Returns(calculationSummaryModels); Dictionary <string, ProviderSourceDataset> sourceDatasets = new Dictionary <string, ProviderSourceDataset>(); Dictionary <string, Dictionary <string, ProviderSourceDataset> > providerSourceDatasets = new Dictionary <string, Dictionary <string, ProviderSourceDataset> > { { providerId, sourceDatasets } }; _providerSourceDatasetsRepository .GetProviderSourceDatasetsByProviderIdsAndRelationshipIds( Arg.Is(specificationId), Arg.Is <IEnumerable <string> >(_ => _ != null && _.Count() == 1 && _.FirstOrDefault() == providerId), Arg.Is <IEnumerable <string> >(_ => _ != null && _.SequenceEqual(dataDefinitionRelationshipIds))) .Returns(providerSourceDatasets); IEnumerable <CalculationAggregation> calculationAggregations = new List <CalculationAggregation>(); _calculationAggregationService .BuildAggregations(Arg.Is <BuildAggregationRequest>(_ => _ != null && _.SpecificationId == specificationId)) .Returns(calculationAggregations); PreviewCalculationRequest previewCalculationRequest = new PreviewCalculationRequest { AssemblyContent = MockData.GetMockAssembly(), PreviewCalculationSummaryModel = previewCalculationSummaryModel }; IActionResult actionResult = await _calculationEnginePreviewService.PreviewCalculationResult(specificationId, providerId, previewCalculationRequest); _calculationEngine .Received(1) .CalculateProviderResults( Arg.Is(allocationModel), specificationId, Arg.Is <IEnumerable <CalculationSummaryModel> >(_ => _.SequenceEqual(expectedCalculationSummaryModels)), Arg.Is <ProviderSummary>(_ => _.UKPRN == providerId), Arg.Is <Dictionary <string, ProviderSourceDataset> >(_ => _.SequenceEqual(sourceDatasets)), Arg.Is <IEnumerable <CalculationAggregation> >(_ => _.SequenceEqual(calculationAggregations))); }
public void CalculateProviderResult_WhenCalculationsAreNotEmpty_ShouldReturnCorrectResult() { // Arrange List <Reference> policySpecificationsForFundingCalc = new List <Reference>() { new Reference("Spec1", "SpecOne"), new Reference("Spec2", "SpecTwo") }; Reference fundingCalcReference = new Reference("CalcF1", "Funding calc 1"); Reference numbercalcReference = new Reference("CalcF2", "Funding calc 2"); Reference booleancalcReference = new Reference("CalcF3", "Funding calc 3"); Reference fundingLineCalcReference = new Reference("FL1", "Funding line calc 1"); CalculationResult fundingCalcReturned = new CalculationResult() { Calculation = fundingCalcReference, Value = 10000 }; CalculationResult fundingCalcReturned2 = new CalculationResult() { Calculation = numbercalcReference, Value = 20000 }; CalculationResult fundingCalcReturned3 = new CalculationResult() { Calculation = booleancalcReference, Value = true }; CalculationResultContainer calculationResultContainer = new CalculationResultContainer(); List <CalculationResult> calculationResults = new List <CalculationResult>() { fundingCalcReturned, fundingCalcReturned2, fundingCalcReturned3 }; calculationResultContainer.CalculationResults = calculationResults; string fundingStreamId = "FS1"; FundingLineResult fundingLineResult = new FundingLineResult { Value = 1000, FundingLine = fundingLineCalcReference, FundingLineFundingStreamId = fundingStreamId }; List <FundingLineResult> fundingLineResults = new List <FundingLineResult> { fundingLineResult }; calculationResultContainer.FundingLineResults = fundingLineResults; IAllocationModel mockAllocationModel = Substitute.For <IAllocationModel>(); mockAllocationModel .Execute(Arg.Any <Dictionary <string, ProviderSourceDataset> >(), Arg.Any <ProviderSummary>()) .Returns(calculationResultContainer); CalculationEngine calculationEngine = CreateCalculationEngine(); ProviderSummary providerSummary = CreateDummyProviderSummary(); BuildProject buildProject = CreateBuildProject(); var nonMatchingCalculationModel = new CalculationSummaryModel() { Id = "Non matching calculation", Name = "Non matching calculation", CalculationType = CalculationType.Template, CalculationValueType = CalculationValueType.Number }; IEnumerable <CalculationSummaryModel> calculationSummaryModels = new[] { new CalculationSummaryModel() { Id = fundingCalcReference.Id, Name = fundingCalcReference.Name, CalculationType = CalculationType.Template, CalculationValueType = CalculationValueType.Number }, new CalculationSummaryModel() { Id = numbercalcReference.Id, Name = numbercalcReference.Name, CalculationType = CalculationType.Template, CalculationValueType = CalculationValueType.Number }, new CalculationSummaryModel() { Id = booleancalcReference.Id, Name = booleancalcReference.Name, CalculationType = CalculationType.Template, CalculationValueType = CalculationValueType.Boolean }, nonMatchingCalculationModel }; // Act var calculateProviderResults = calculationEngine.CalculateProviderResults(mockAllocationModel, buildProject.SpecificationId, calculationSummaryModels, providerSummary, new Dictionary <string, ProviderSourceDataset>()); ProviderResult result = calculateProviderResults; // Assert result.Provider.Should().Be(providerSummary); result.SpecificationId.Should().BeEquivalentTo(buildProject.SpecificationId); result.Id.Should().BeEquivalentTo(GenerateId(providerSummary.Id, buildProject.SpecificationId)); result.CalculationResults.Should().HaveCount(3); result.FundingLineResults.Should().HaveCount(1); CalculationResult fundingCalcResult = result.CalculationResults.First(cr => cr.Calculation.Id == fundingCalcReference.Id); fundingCalcResult.Calculation.Should().BeEquivalentTo(fundingCalcReference); fundingCalcResult.CalculationType.Should().BeEquivalentTo(CalculationType.Template); fundingCalcResult.Value.Should().Be(fundingCalcReturned.Value); fundingCalcResult.CalculationDataType.Should().Be(CalculationDataType.Decimal); CalculationResult numberCalcResult = result.CalculationResults.First(cr => cr.Calculation.Id == numbercalcReference.Id); numberCalcResult.Calculation.Should().BeEquivalentTo(numbercalcReference); numberCalcResult.CalculationType.Should().BeEquivalentTo(CalculationType.Template); numberCalcResult.Value.Should().Be(fundingCalcReturned2.Value); numberCalcResult.CalculationDataType.Should().Be(CalculationDataType.Decimal); CalculationResult booleanCalcResult = result.CalculationResults.First(cr => cr.Calculation.Id == booleancalcReference.Id); booleanCalcResult.Calculation.Should().BeEquivalentTo(booleancalcReference); booleanCalcResult.CalculationType.Should().BeEquivalentTo(CalculationType.Template); booleanCalcResult.Value.Should().Be(fundingCalcReturned3.Value); booleanCalcResult.CalculationDataType.Should().Be(CalculationDataType.Boolean); FundingLineResult fundingLineCalcResult = result.FundingLineResults.First(cr => cr.FundingLine.Id == fundingLineCalcReference.Id); fundingLineCalcResult.FundingLine.Should().BeEquivalentTo(fundingLineCalcReference); fundingLineCalcResult.Value.Should().Be(fundingLineResult.Value); fundingLineCalcResult.FundingLineFundingStreamId.Should().Be(fundingStreamId); }