diff --git a/.github/workflows/master-build-and-verify-pull-request.yml b/.github/workflows/master-build-and-verify-pull-request.yml index 63c3a74..121870d 100644 --- a/.github/workflows/master-build-and-verify-pull-request.yml +++ b/.github/workflows/master-build-and-verify-pull-request.yml @@ -10,7 +10,7 @@ on: jobs: build: - runs-on: windows-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/master_oddscollector-functions.yml b/.github/workflows/master_oddscollector-functions.yml new file mode 100644 index 0000000..d6047c3 --- /dev/null +++ b/.github/workflows/master_oddscollector-functions.yml @@ -0,0 +1,42 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy dotnet core app to Azure Function App - oddscollector-functions + +on: + push: + branches: + - master + workflow_dispatch: + +env: + AZURE_FUNCTIONAPP_PACKAGE_PATH: 'OddsCollector.Functions' # set this to the path to your web app project, defaults to the repository root + DOTNET_VERSION: '8.0.x' # set this to the dotnet version to use + +jobs: + build-and-deploy: + runs-on: windows-latest + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: Setup DotNet ${{ env.DOTNET_VERSION }} Environment + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: 'Resolve Project Dependencies Using Dotnet' + shell: pwsh + run: | + pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}' + dotnet build --configuration Release --output ./output + popd + + - name: 'Run Azure Functions Action' + uses: Azure/functions-action@v1 + id: fa + with: + app-name: 'oddscollector-functions' + slot-name: 'Production' + package: '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output' + publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_84942172C4BE4DDDBE1C48EFB32A2A9A }} diff --git a/OddsCollector.Functions.Tests/CommunicationServices/EmailSenderOptionsTests.cs b/OddsCollector.Functions.Tests/CommunicationServices/EmailSenderOptionsTests.cs deleted file mode 100644 index 2221e06..0000000 --- a/OddsCollector.Functions.Tests/CommunicationServices/EmailSenderOptionsTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -using OddsCollector.Functions.CommunicationServices.Configuration; - -namespace OddsCollector.Functions.Tests.CommunicationServices; - -[Parallelizable(ParallelScope.All)] -internal class EmailSenderOptionsTests -{ - [Test] - public void SetRecipientAddress_WithValidAddress_ReturnsValidInstance() - { - const string address = "test@example.com"; - - var options = new EmailSenderOptions(); - - options.SetRecipientAddress(address); - - options.RecipientAddress.Should().Be(address); - } - - [TestCase("")] - [TestCase(null)] - public void SetRecipientAddress_WithNullOrEmptyRecipientAddress_ThrowsException(string? recipientAddress) - { - var options = new EmailSenderOptions(); - - var action = () => - { - options.SetRecipientAddress(recipientAddress); - }; - - action.Should().Throw().WithParameterName(nameof(recipientAddress)); - } - - [Test] - public void SetSenderAddress_WithValidSenderAddress_ReturnsValidInstance() - { - const string address = "test@example.com"; - - var options = new EmailSenderOptions(); - - options.SetSenderAddress(address); - - options.SenderAddress.Should().Be(address); - } - - [TestCase("")] - [TestCase(null)] - public void SetSenderAddress_WithNullOrEmptySenderAddress_ThrowsException(string? senderAddress) - { - var options = new EmailSenderOptions(); - - var action = () => - { - options.SetSenderAddress(senderAddress); - }; - - action.Should().Throw().WithParameterName(nameof(senderAddress)); - } - - [Test] - public void SetSubject_WithValidSubject_ReturnsValidInstance() - { - const string subject = nameof(subject); - - var options = new EmailSenderOptions(); - - options.SetSubject(subject); - - options.Subject.Should().Be(subject); - } - - [TestCase("")] - [TestCase(null)] - public void SetSubject_WithNullOrEmptySubject_ThrowsException(string? subject) - { - var options = new EmailSenderOptions(); - - var action = () => - { - options.SetSubject(subject); - }; - - action.Should().Throw().WithParameterName(nameof(subject)); - } -} diff --git a/OddsCollector.Functions.Tests/CommunicationServices/EmailSenderTests.cs b/OddsCollector.Functions.Tests/CommunicationServices/EmailSenderTests.cs deleted file mode 100644 index 15caeda..0000000 --- a/OddsCollector.Functions.Tests/CommunicationServices/EmailSenderTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Text.Json; -using Azure; -using Azure.Communication.Email; -using Microsoft.Extensions.Options; -using OddsCollector.Functions.CommunicationServices; -using OddsCollector.Functions.CommunicationServices.Configuration; -using OddsCollector.Functions.Models; - -namespace OddsCollector.Functions.Tests.CommunicationServices; - -[Parallelizable(ParallelScope.All)] -internal class EmailSenderTests -{ - [Test] - public void Constructor_WithValidDependencies_ReturnsNewInstance() - { - var optionsStub = Substitute.For>(); - optionsStub.Value.Returns(new EmailSenderOptions()); - var clientStub = Substitute.For(); - - var sender = new EmailSender(optionsStub, clientStub); - - sender.Should().NotBeNull(); - } - - [Test] - public void Constructor_WithNullOptions_ThrowsException() - { - var clientStub = Substitute.For(); - - var action = () => - { - _ = new EmailSender(null, clientStub); - }; - - action.Should().Throw().WithParameterName("options"); - } - - [Test] - public void Constructor_WithNullEmailSender_ThrowsException() - { - var optionsStub = Substitute.For>(); - optionsStub.Value.Returns(new EmailSenderOptions()); - - var action = () => - { - _ = new EmailSender(optionsStub, null); - }; - - action.Should().Throw().WithParameterName("client"); - } - - [Test] - public async Task SendEmailAsync_WithValidOptions_SendsEmails() - { - List predictions = [ - new() - { - AwayTeam = "away", - Bookmaker = "bookmaker", - CommenceTime = DateTime.UtcNow, - HomeTeam = "Home", - Id = "id", - Strategy = "strategy", - Timestamp = DateTime.UtcNow, - TraceId = Guid.NewGuid(), - Winner = "winner" - } - ]; - - const string recipientAddress = "test@example.com"; - const string senderAddress = "test2@example.com"; - const string subject = "test"; - - var optionsStub = Substitute.For>(); - optionsStub.Value.Returns(new EmailSenderOptions - { - RecipientAddress = recipientAddress, - SenderAddress = senderAddress, - Subject = subject - }); - - var clientMock = Substitute.For(); - - var sender = new EmailSender(optionsStub, clientMock); - - var token = new CancellationToken(); - - await sender.SendEmailAsync(predictions, token).ConfigureAwait(false); - - var received = clientMock.ReceivedCalls().ToList(); - - received.Should().NotBeNull(); - received.Should().HaveCount(1); - - var firstReceived = received.First(); - - firstReceived.GetMethodInfo().Name.Should().Be("SendAsync"); - - var firstReceivedArguments = firstReceived.GetArguments(); - - firstReceivedArguments.Should().NotBeNull(); - firstReceivedArguments[0].Should().Be(WaitUntil.Completed); - firstReceivedArguments[1].Should().Be(senderAddress); - firstReceivedArguments[2].Should().Be(recipientAddress); - firstReceivedArguments[3].Should().Be(subject); - firstReceivedArguments[6].Should().Be(token); - - var deserialized = JsonSerializer.Deserialize>((string)firstReceivedArguments[4]!); - - deserialized![0].Id.Should().Be(predictions[0].Id); - } -} diff --git a/OddsCollector.Functions.Tests/CosmosDb/ContainerFactoryTests.cs b/OddsCollector.Functions.Tests/CosmosDb/ContainerFactoryTests.cs deleted file mode 100644 index e52b8e4..0000000 --- a/OddsCollector.Functions.Tests/CosmosDb/ContainerFactoryTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using OddsCollector.Functions.CosmosDb; - -namespace OddsCollector.Functions.Tests.CosmosDb; - -[Parallelizable(ParallelScope.All)] -internal class ContainerFactoryTests -{ - [Test] - public void CreateContainer_WithValidParameters_ReturnsNewInstance() - { - string databaseId = nameof(databaseId); - - var container = ContainerFactory.CreateContainer( - "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test", databaseId, "containerId"); - - container.Should().NotBeNull(); - container.Database.Should().NotBeNull(); - container.Database.Id.Should().NotBeNull().And.Be(databaseId); - } - - [TestCase("")] - [TestCase(null)] - public void CreateContainer_WithNullOrEmptyConnectionString_ThrowsException(string? connectionString) - { - var action = () => - { - _ = ContainerFactory.CreateContainer(connectionString, "databaseId", "containerId"); - }; - - action.Should().Throw().WithParameterName(nameof(connectionString)); - } - - [TestCase("")] - [TestCase(null)] - public void CreateContainer_WithNullOrEmptyDatabaseId_ThrowsException(string? databaseId) - { - var action = () => - { - _ = ContainerFactory.CreateContainer( - "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test", databaseId, "containerId"); - }; - - action.Should().Throw().WithParameterName(nameof(databaseId)); - } - - [TestCase("")] - [TestCase(null)] - public void CreateContainer_WithNullOrEmptyContainerId_ThrowsException(string? containerId) - { - var action = () => - { - _ = ContainerFactory.CreateContainer( - "AccountEndpoint=https://test.documents.azure.com:443/;AccountKey=test", "databaseId", containerId); - }; - - action.Should().Throw().WithParameterName(nameof(containerId)); - } -} diff --git a/OddsCollector.Functions.Tests/CosmosDb/CosmosDbClientTests.cs b/OddsCollector.Functions.Tests/CosmosDb/CosmosDbClientTests.cs deleted file mode 100644 index 785f8b8..0000000 --- a/OddsCollector.Functions.Tests/CosmosDb/CosmosDbClientTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Azure.Cosmos; -using OddsCollector.Functions.CosmosDb; - -namespace OddsCollector.Functions.Tests.CosmosDb; - -[Parallelizable(ParallelScope.All)] -internal class CosmosDbClientTests -{ - [Test] - public void Constructor_WithValidDependencies_ReturnsNewInstance() - { - var containerStub = Substitute.For(); - - var result = new CosmosDbClient(containerStub); - - result.Should().NotBeNull(); - } - - [Test] - public void Constructor_WithNullContainer_ThrowsException() - { - var action = () => - { - _ = new CosmosDbClient(null); - }; - - action.Should().Throw().WithParameterName("container"); - } - - // todo: test for GetEventPredictionsAsync() - hard to mock all the dependencies atm -} diff --git a/OddsCollector.Functions.Tests/Functions/EventResultsFunctionTests.cs b/OddsCollector.Functions.Tests/Functions/EventResultsFunctionTests.cs index 65acf06..9e21d42 100644 --- a/OddsCollector.Functions.Tests/Functions/EventResultsFunctionTests.cs +++ b/OddsCollector.Functions.Tests/Functions/EventResultsFunctionTests.cs @@ -49,7 +49,7 @@ public void Constructor_WithNullLogger_ThrowsException() [Test] public async Task Run_WithValidParameters_ReturnsEventResults() { - IEnumerable expectedEventResults = []; + IEnumerable expectedEventResults = new List(); var loggerStub = Substitute.For>(); diff --git a/OddsCollector.Functions.Tests/Functions/NotificationFunctionTests.cs b/OddsCollector.Functions.Tests/Functions/NotificationFunctionTests.cs deleted file mode 100644 index fa5d77c..0000000 --- a/OddsCollector.Functions.Tests/Functions/NotificationFunctionTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Microsoft.Extensions.Logging; -using NSubstitute.ExceptionExtensions; -using OddsCollector.Functions.CommunicationServices; -using OddsCollector.Functions.CosmosDb; -using OddsCollector.Functions.Functions; -using OddsCollector.Functions.Models; - -namespace OddsCollector.Functions.Tests.Functions; - -[Parallelizable(ParallelScope.All)] -internal class NotificationFunctionTests -{ - [Test] - public void Constructor_WithValidDependencies_ReturnsNewInstance() - { - var loggerStub = Substitute.For>(); - var emailSenderStub = Substitute.For(); - var cosmosDbClientStub = Substitute.For(); - - var function = new NotificationFunction(loggerStub, emailSenderStub, cosmosDbClientStub); - - function.Should().NotBeNull(); - } - - [Test] - public void Constructor_WithNullLogger_ThrowsException() - { - var emailSenderStub = Substitute.For(); - var cosmosDbClientStub = Substitute.For(); - - var action = () => - { - _ = new NotificationFunction(null, emailSenderStub, cosmosDbClientStub); - }; - - action.Should().Throw().WithParameterName("logger"); - } - - [Test] - public void Constructor_WithNullEmailClient_ThrowsException() - { - var loggerStub = Substitute.For>(); - var cosmosDbClientStub = Substitute.For(); - - var action = () => - { - _ = new NotificationFunction(loggerStub, null, cosmosDbClientStub); - }; - - action.Should().Throw().WithParameterName("sender"); - } - - [Test] - public void Constructor_WithNullCosmosDbClient_ThrowsException() - { - var loggerStub = Substitute.For>(); - var emailSenderStub = Substitute.For(); - - var action = () => - { - _ = new NotificationFunction(loggerStub, emailSenderStub, null); - }; - - action.Should().Throw().WithParameterName("client"); - } - - [Test] - public async Task Run_WithValidParameters_SendsEmails() - { - var loggerStub = Substitute.For>(); - - var emailSenderMock = Substitute.For(); - - IEnumerable predictons = []; - var cosmosDbClientMock = Substitute.For(); - cosmosDbClientMock.GetEventPredictionsAsync(Arg.Any()).Returns(Task.FromResult(predictons)); - - var function = new NotificationFunction(loggerStub, emailSenderMock, cosmosDbClientMock); - - var token = new CancellationToken(); - - await function.Run(token).ConfigureAwait(false); - - await emailSenderMock.Received().SendEmailAsync(predictons, token); - await cosmosDbClientMock.Received().GetEventPredictionsAsync(token); - } - - [Test] - public async Task Run_WithException_DoesntFail() - { - var loggerMock = Substitute.For>(); - - var emailSenderMock = Substitute.For(); - - var exception = new Exception(); - - var cosmosDbClientMock = Substitute.For(); - cosmosDbClientMock.GetEventPredictionsAsync(Arg.Any()).Throws(exception); - - var function = new NotificationFunction(loggerMock, emailSenderMock, cosmosDbClientMock); - - var token = new CancellationToken(); - - await function.Run(token).ConfigureAwait(false); - - loggerMock.Received().LogError(exception, "Failed to send e-mail notification"); - } -} diff --git a/OddsCollector.Functions.Tests/Functions/PredictionFunctionTests.cs b/OddsCollector.Functions.Tests/Functions/PredictionFunctionTests.cs index 66baef4..d800b53 100644 --- a/OddsCollector.Functions.Tests/Functions/PredictionFunctionTests.cs +++ b/OddsCollector.Functions.Tests/Functions/PredictionFunctionTests.cs @@ -76,7 +76,7 @@ public async Task Run_WithServiceBusMessage_ReturnsEventPrediction() .SetTimestamp(DateTime.Now) .SetWinner("Manchester City").Instance; - ServiceBusReceivedMessage[] receivedmessages = + ServiceBusReceivedMessage[] receivedMessages = [ServiceBusReceivedMessageFactory.CreateFromObject(upcomingEvent)]; var actionsMock = Substitute.For(); @@ -88,12 +88,12 @@ public async Task Run_WithServiceBusMessage_ReturnsEventPrediction() var token = new CancellationToken(); - var prediction = await function.Run(receivedmessages, actionsMock, token).ConfigureAwait(false); + var prediction = await function.Run(receivedMessages, actionsMock, token).ConfigureAwait(false); prediction.Should().NotBeNull().And.HaveCount(1); prediction[0].Should().NotBeNull().And.Be(expectedPrediction); - await actionsMock.Received(Quantity.Exactly(1)).CompleteMessageAsync(receivedmessages[0], token); + await actionsMock.Received(Quantity.Exactly(1)).CompleteMessageAsync(receivedMessages[0], token); } [Test] @@ -134,7 +134,7 @@ public async Task Run_WithInvalidItems_ReturnsSucessfuallyProcessedEventPredicti .SetTimestamp(DateTime.Now) .SetWinner("Manchester City").Instance; - ServiceBusReceivedMessage[] receivedmessages = + ServiceBusReceivedMessage[] receivedMessages = [ ServiceBusReceivedMessageFactory.CreateFromObject(badUpcomingEvent), ServiceBusReceivedMessageFactory.CreateFromObject(goodUpcomingEvent) @@ -146,18 +146,18 @@ public async Task Run_WithInvalidItems_ReturnsSucessfuallyProcessedEventPredicti var strategyStub = Substitute.For(); strategyStub.GetPrediction(Arg.Any(), Arg.Any()) - .Returns(x => { throw exception; }, x => expectedPrediction); + .Returns(x => throw exception, x => expectedPrediction); var function = new PredictionFunction(loggerMock, strategyStub); var token = new CancellationToken(); - var prediction = await function.Run(receivedmessages, actionsMock, token).ConfigureAwait(false); + var prediction = await function.Run(receivedMessages, actionsMock, token).ConfigureAwait(false); prediction.Should().NotBeNull().And.HaveCount(1); prediction[0].Should().NotBeNull().And.Be(expectedPrediction); - await actionsMock.Received(Quantity.Exactly(1)).CompleteMessageAsync(receivedmessages[1], token); + await actionsMock.Received(Quantity.Exactly(1)).CompleteMessageAsync(receivedMessages[1], token); var receivedCalls = loggerMock.ReceivedCalls().ToList(); diff --git a/OddsCollector.Functions.Tests/Functions/UpcomingEventsFunctionTest.cs b/OddsCollector.Functions.Tests/Functions/UpcomingEventsFunctionTest.cs index 070de1b..428f605 100644 --- a/OddsCollector.Functions.Tests/Functions/UpcomingEventsFunctionTest.cs +++ b/OddsCollector.Functions.Tests/Functions/UpcomingEventsFunctionTest.cs @@ -49,7 +49,7 @@ public void Constructor_WithNullOddsClient_ThrowsException() [Test] public async Task Run_WithValidParameters_ReturnsEventResults() { - IEnumerable expectedEventResults = []; + IEnumerable expectedEventResults = new List(); var loggerStub = Substitute.For>(); diff --git a/OddsCollector.Functions.Tests/OddsApi/Configuration/OddsApiOptionsTests.cs b/OddsCollector.Functions.Tests/OddsApi/Configuration/OddsApiOptionsTests.cs index afbd1d4..0948fab 100644 --- a/OddsCollector.Functions.Tests/OddsApi/Configuration/OddsApiOptionsTests.cs +++ b/OddsCollector.Functions.Tests/OddsApi/Configuration/OddsApiOptionsTests.cs @@ -8,7 +8,7 @@ internal sealed class OddsApiOptionsTests [Test] public void SetLeagues_WithValidLeague_ReturnsThisLeague() { - string league = nameof(league); + const string league = nameof(league); var options = new OddsApiClientOptions(); options.AddLeagues(league); @@ -20,7 +20,7 @@ public void SetLeagues_WithValidLeague_ReturnsThisLeague() [Test] public void SetLeagues_WithValidLeagues_ReturnsTheseLeagues() { - var leagues = "league1;league2"; + const string leagues = "league1;league2"; var options = new OddsApiClientOptions(); options.AddLeagues(leagues); @@ -33,7 +33,7 @@ public void SetLeagues_WithValidLeagues_ReturnsTheseLeagues() [Test] public void SetLeagues_WithOneEmptyLeague_ReturnsNonEmptyLeagues() { - var leagues = "league1;;league2"; + const string leagues = "league1;;league2"; var options = new OddsApiClientOptions(); options.AddLeagues(leagues); @@ -46,7 +46,7 @@ public void SetLeagues_WithOneEmptyLeague_ReturnsNonEmptyLeagues() [Test] public void SetLeagues_WithDuplicatedLeagues_ReturnsNewInstance() { - var leagues = "league1;league1"; + const string leagues = "league1;league1"; var options = new OddsApiClientOptions(); options.AddLeagues(leagues); @@ -71,7 +71,7 @@ public void SetLeagues_WithNullOrEmptyLeagues_ThrowsException(string? leaguesStr [Test] public void SetApiKey_WithValidKey_ReturnsThisKey() { - string key = nameof(key); + const string key = nameof(key); var options = new OddsApiClientOptions(); options.SetApiKey(key); diff --git a/OddsCollector.Functions.Tests/OddsApi/Configuration/ServiceCollectionExtensionsTests.cs b/OddsCollector.Functions.Tests/OddsApi/Configuration/ServiceCollectionExtensionsTests.cs index e9d881c..a20c332 100644 --- a/OddsCollector.Functions.Tests/OddsApi/Configuration/ServiceCollectionExtensionsTests.cs +++ b/OddsCollector.Functions.Tests/OddsApi/Configuration/ServiceCollectionExtensionsTests.cs @@ -18,30 +18,30 @@ public void AddOddsApiClientWithDependencies_AddsProperlyConfiguredOddsApiClient var options = services.FirstOrDefault( x => x.ServiceType == typeof(IConfigureOptions) - && x.Lifetime == ServiceLifetime.Singleton); + && x.Lifetime == ServiceLifetime.Singleton); options.Should().NotBeNull(); var httpClient = services.FirstOrDefault( x => x.ServiceType == typeof(HttpClient) - && x.Lifetime == ServiceLifetime.Transient); + && x.Lifetime == ServiceLifetime.Transient); httpClient.Should().NotBeNull(); var client = services.FirstOrDefault( x => x.ImplementationType == typeof(Client) - && x.ServiceType == typeof(IClient) - && x.Lifetime == ServiceLifetime.Singleton); + && x.ServiceType == typeof(IClient) + && x.Lifetime == ServiceLifetime.Singleton); client.Should().NotBeNull(); var oddsApiClient = services.FirstOrDefault( x => x.ImplementationType == typeof(OddsApiClient) - && x.ServiceType == typeof(IOddsApiClient) - && x.Lifetime == ServiceLifetime.Singleton); + && x.ServiceType == typeof(IOddsApiClient) + && x.Lifetime == ServiceLifetime.Singleton); oddsApiClient.Should().NotBeNull(); } diff --git a/OddsCollector.Functions.Tests/OddsApi/Converter/OddsApiObjectConverterTests.cs b/OddsCollector.Functions.Tests/OddsApi/Converter/OddsApiObjectConverterTests.cs index 335e3b1..81fc845 100644 --- a/OddsCollector.Functions.Tests/OddsApi/Converter/OddsApiObjectConverterTests.cs +++ b/OddsCollector.Functions.Tests/OddsApi/Converter/OddsApiObjectConverterTests.cs @@ -115,7 +115,8 @@ public void ToUpcomingEvents_WithNullBookmakers_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawUpcomingEvents = [ + List rawUpcomingEvents = + [ new TestAnonymous2Builder().SetDefaults().SetBookmakers(null).Instance ]; @@ -133,7 +134,8 @@ public void ToUpcomingEvents_WithNullOrEmptyAwayTeam_ThrowsException(string? awa { var converter = new OddsApiObjectConverter(); - List rawUpcomingEvents = [ + List rawUpcomingEvents = + [ new TestAnonymous2Builder().SetDefaults().SetAwayTeam(awayTeam).Instance ]; @@ -173,20 +175,20 @@ public void ToUpcomingEvents_WithNullOrEmptyBookmakerKey_ThrowsException(string? List rawUpcomingEvents = [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( - [ - new() + [ + new Bookmakers { Key = bookmaker, Markets = [ - new() + new Markets2 { Key = Markets2Key.H2h, Outcomes = [ - new() { Name = "Liverpool", Price = 4.08 }, - new() { Name = "Manchester City", Price = 1.7 }, - new() { Name = "Draw", Price = 3.82 } + new Outcome { Name = "Liverpool", Price = 4.08 }, + new Outcome { Name = "Manchester City", Price = 1.7 }, + new Outcome { Name = "Draw", Price = 3.82 } ] } ] @@ -211,7 +213,7 @@ public void ToUpcomingEvents_WithNullMarkets_ThrowsException() List rawUpcomingEvents = [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( - [new() { Key = "onexbet", Markets = null }] + [new Bookmakers { Key = "onexbet", Markets = null }] ).Instance ]; @@ -232,19 +234,19 @@ public void ToUpcomingEvents_WithNullMarketKey_ThrowsException() [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( [ - new() + new Bookmakers { Key = "onexbet", Markets = [ - new() + new Markets2 { Key = null, Outcomes = [ - new() { Name = "Liverpool", Price = 4.33 }, - new() { Name = "Manchester City", Price = 1.7 }, - new() { Name = "Draw", Price = 4.33 } + new Outcome { Name = "Liverpool", Price = 4.33 }, + new Outcome { Name = "Manchester City", Price = 1.7 }, + new Outcome { Name = "Draw", Price = 4.33 } ] } ] @@ -269,7 +271,7 @@ public void ToUpcomingEvents_WithEmptyMarkets_ThrowsException() List rawUpcomingEvents = [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( - [new() { Key = "onexbet", Markets = [] }] + [new Bookmakers { Key = "onexbet", Markets = [] }] ).Instance ]; @@ -286,15 +288,15 @@ public void ToUpcomingEvents_WithNullOutcomes_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawUpcomingEvents = [ + List rawUpcomingEvents = + [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( - [ - new() + [ + new Bookmakers { - Key = "onexbet", - Markets = [new() { Key = Markets2Key.H2h, Outcomes = null }] + Key = "onexbet", Markets = [new Markets2 { Key = Markets2Key.H2h, Outcomes = null }] } - ] + ] ).Instance ]; @@ -311,17 +313,19 @@ public void ToUpcomingEvents_WithEmptyOutcomes_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawUpcomingEvents = [ + List rawUpcomingEvents = + [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( - [ - new() + [ + new Bookmakers { Key = "onexbet", - Markets = [ - new() { Key = Markets2Key.H2h, Outcomes = [] } + Markets = + [ + new Markets2 { Key = Markets2Key.H2h, Outcomes = [] } ] } - ] + ] ).Instance ]; @@ -338,24 +342,27 @@ public void ToUpcomingEvents_WithOneOutcome_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawUpcomingEvents = [ + List rawUpcomingEvents = + [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( - [ - new() + [ + new Bookmakers { Key = "onexbet", - Markets = [ - new() + Markets = + [ + new Markets2 { Key = Markets2Key.H2h, - Outcomes = [ - new() { Name = "Manchester City", Price = 1.7 }, - new() { Name = "Draw", Price = 4.33 } + Outcomes = + [ + new Outcome { Name = "Manchester City", Price = 1.7 }, + new Outcome { Name = "Draw", Price = 4.33 } ] } ] } - ] + ] ).Instance ]; @@ -376,17 +383,19 @@ public void ToUpcomingEvents_WithNullHomePrice_ThrowsException() [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( [ - new() + new Bookmakers { Key = "onexbet", - Markets = [ - new() + Markets = + [ + new Markets2 { Key = Markets2Key.H2h, - Outcomes = [ - new() { Name = "Liverpool", Price = 4.08 }, - new() { Name = "Manchester City", Price = null }, - new() { Name = "Draw", Price = 3.82 } + Outcomes = + [ + new Outcome { Name = "Liverpool", Price = 4.08 }, + new Outcome { Name = "Manchester City", Price = null }, + new Outcome { Name = "Draw", Price = 3.82 } ] } ] @@ -408,25 +417,28 @@ public void ToUpcomingEvents_WithNullAwayPrice_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawUpcomingEvents = [ + List rawUpcomingEvents = + [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( - [ - new() + [ + new Bookmakers { Key = "onexbet", - Markets = [ - new() + Markets = + [ + new Markets2 { Key = Markets2Key.H2h, - Outcomes = [ - new() { Name = "Liverpool", Price = null }, - new() { Name = "Manchester City", Price = 1.7 }, - new() { Name = "Draw", Price = 3.82 } + Outcomes = + [ + new Outcome { Name = "Liverpool", Price = null }, + new Outcome { Name = "Manchester City", Price = 1.7 }, + new Outcome { Name = "Draw", Price = 3.82 } ] } ] } - ] + ] ).Instance ]; @@ -443,25 +455,28 @@ public void ToUpcomingEvents_WithNullDrawPrice_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawUpcomingEvents = [ + List rawUpcomingEvents = + [ new TestAnonymous2Builder().SetDefaults().SetBookmakers( - [ - new() + [ + new Bookmakers { Key = "onexbet", - Markets = [ - new() + Markets = + [ + new Markets2 { Key = Markets2Key.H2h, - Outcomes = [ - new() { Name = "Liverpool", Price = 4.08 }, - new() { Name = "Manchester City", Price = 1.7 }, - new() { Name = "Draw", Price = null } + Outcomes = + [ + new Outcome { Name = "Liverpool", Price = 4.08 }, + new Outcome { Name = "Manchester City", Price = 1.7 }, + new Outcome { Name = "Draw", Price = null } ] } ] } - ] + ] ).Instance ]; @@ -481,18 +496,19 @@ public void ToEventResults_WithCompletedEvents_ReturnsConvertedEvents() var timestamp = DateTime.UtcNow; var traceId = Guid.NewGuid(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = "Manchester City", Score = "1" }, - new() { Name = "Liverpool", Score = "0" } + new ScoreModel { Name = "Manchester City", Score = "1" }, + new ScoreModel { Name = "Liverpool", Score = "0" } ]).Instance, new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = "Manchester City", Score = "0" }, - new() { Name = "Liverpool", Score = "1" } + new ScoreModel { Name = "Manchester City", Score = "0" }, + new ScoreModel { Name = "Liverpool", Score = "1" } ]).Instance, new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = "Manchester City", Score = "1" }, - new() { Name = "Liverpool", Score = "1" } + new ScoreModel { Name = "Manchester City", Score = "1" }, + new ScoreModel { Name = "Liverpool", Score = "1" } ]).Instance ]; @@ -558,7 +574,8 @@ public void ToEventResults_WithUncompletedEvents_ReturnsNoEvents() { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetCompleted(null).Instance, new TestAnonymous3Builder().SetDefaults().SetCompleted(false).Instance ]; @@ -573,7 +590,8 @@ public void ToEventResults_WithEmptyScores_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetScores(null).Instance ]; @@ -591,7 +609,8 @@ public void ToEventResults_WithNullOrEmptyHomeTeam_ThrowsException(string? homeT { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetHomeTeam(homeTeam).Instance ]; @@ -609,7 +628,8 @@ public void ToEventResults_WithNullOrEmptyAwayTeam_ThrowsException(string? awayT { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetAwayTeam(awayTeam).Instance ]; @@ -627,10 +647,11 @@ public void ToEventResults_WithNullOrEmptyTeamName_ThrowsException(string? name) { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = name, Score = "1" }, - new() { Name = name, Score = "1" } + new ScoreModel { Name = name, Score = "1" }, + new ScoreModel { Name = name, Score = "1" } ]).Instance ]; @@ -648,10 +669,11 @@ public void ToEventResults_WithNullOrEmptyScoreValue_ThrowsException(string? sco { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = "Manchester City", Score = score }, - new() { Name = "Liverpool", Score = score } + new ScoreModel { Name = "Manchester City", Score = score }, + new ScoreModel { Name = "Liverpool", Score = score } ]).Instance ]; @@ -668,11 +690,12 @@ public void ToEventResults_WithDuplicatedScore_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = "Manchester City", Score = "1" }, - new() { Name = "Liverpool", Score = "1" }, - new() { Name = "Liverpool", Score = "1" } + new ScoreModel { Name = "Manchester City", Score = "1" }, + new ScoreModel { Name = "Liverpool", Score = "1" }, + new ScoreModel { Name = "Liverpool", Score = "1" } ]).Instance ]; @@ -689,11 +712,12 @@ public void ToEventResults_WithExtraScore_ReturnsEventResult() { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = "Manchester City", Score = "1" }, - new() { Name = "Liverpool", Score = "0" }, - new() { Name = "Nottingham Forest", Score = "1" } + new ScoreModel { Name = "Manchester City", Score = "1" }, + new ScoreModel { Name = "Liverpool", Score = "0" }, + new ScoreModel { Name = "Nottingham Forest", Score = "1" } ]).Instance ]; @@ -709,9 +733,10 @@ public void ToEventResults_WithoutAwayTeamScore_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = "Manchester City", Score = "1" } + new ScoreModel { Name = "Manchester City", Score = "1" } ]).Instance ]; @@ -728,9 +753,10 @@ public void ToEventResults_WithoutHomeTeamScore_ThrowsException() { var converter = new OddsApiObjectConverter(); - List rawEventResults = [ + List rawEventResults = + [ new TestAnonymous3Builder().SetDefaults().SetScores([ - new() { Name = "Liverpool", Score = "1" } + new ScoreModel { Name = "Liverpool", Score = "1" } ]).Instance ]; diff --git a/OddsCollector.Functions.Tests/OddsApi/Converter/TestAnonymous2Builder.cs b/OddsCollector.Functions.Tests/OddsApi/Converter/TestAnonymous2Builder.cs index 48d5b73..229f0ab 100644 --- a/OddsCollector.Functions.Tests/OddsApi/Converter/TestAnonymous2Builder.cs +++ b/OddsCollector.Functions.Tests/OddsApi/Converter/TestAnonymous2Builder.cs @@ -10,33 +10,38 @@ internal class TestAnonymous2Builder public const string DefaultId = "4acd8f2675ca847ba33eea3664f6c0bb"; public static readonly DateTime DefaultCommenceTime = new(2023, 11, 25, 12, 30, 0); - public static readonly ICollection DefaultBookmakers = [ - new() + public static readonly ICollection DefaultBookmakers = + [ + new Bookmakers { Key = "betclic", - Markets = [ - new() + Markets = + [ + new Markets2 { Key = Markets2Key.H2h, - Outcomes = [ - new() { Name = "Liverpool", Price = 4.08 }, - new() { Name = "Manchester City", Price = 1.7 }, - new() { Name = "Draw", Price = 3.82 } + Outcomes = + [ + new Outcome { Name = "Liverpool", Price = 4.08 }, + new Outcome { Name = "Manchester City", Price = 1.7 }, + new Outcome { Name = "Draw", Price = 3.82 } ] } ] }, - new() + new Bookmakers { Key = "sport888", - Markets = [ - new() + Markets = + [ + new Markets2 { Key = Markets2Key.H2h, - Outcomes = [ - new() { Name = "Liverpool", Price = 4.33 }, - new() { Name = "Manchester City", Price = 1.7 }, - new() { Name = "Draw", Price = 4.33 } + Outcomes = + [ + new Outcome { Name = "Liverpool", Price = 4.33 }, + new Outcome { Name = "Manchester City", Price = 1.7 }, + new Outcome { Name = "Draw", Price = 4.33 } ] } ] diff --git a/OddsCollector.Functions.Tests/OddsApi/Converter/TestAnonymous3Builder.cs b/OddsCollector.Functions.Tests/OddsApi/Converter/TestAnonymous3Builder.cs index 5077f81..d6ee987 100644 --- a/OddsCollector.Functions.Tests/OddsApi/Converter/TestAnonymous3Builder.cs +++ b/OddsCollector.Functions.Tests/OddsApi/Converter/TestAnonymous3Builder.cs @@ -11,9 +11,10 @@ internal sealed class TestAnonymous3Builder public const string DefaultId = "4acd8f2675ca847ba33eea3664f6c0bb"; public static readonly DateTime DefaultCommenceTime = new(2023, 11, 25, 12, 30, 0); - public static readonly ICollection DefaultScores = [ - new() { Name = "Manchester City", Score = "1" }, - new() { Name = "Liverpool", Score = "0" } + public static readonly ICollection DefaultScores = + [ + new ScoreModel { Name = "Manchester City", Score = "1" }, + new ScoreModel { Name = "Liverpool", Score = "0" } ]; public Anonymous3 Instance { get; } = new(); diff --git a/OddsCollector.Functions.Tests/OddsApi/OddsApiClientTests.cs b/OddsCollector.Functions.Tests/OddsApi/OddsApiClientTests.cs index 8945085..9a76861 100644 --- a/OddsCollector.Functions.Tests/OddsApi/OddsApiClientTests.cs +++ b/OddsCollector.Functions.Tests/OddsApi/OddsApiClientTests.cs @@ -86,11 +86,12 @@ public void Constructor_WithNullConverter_ThrowsException() [Test] public async Task GetUpcomingEventsAsync_WithLeagues_ReturnsUpcomingEvents() { - ICollection rawUpcomingEvents = [new()]; + ICollection rawUpcomingEvents = [new Anonymous2()]; var webApiClientMock = Substitute.For(); webApiClientMock .OddsAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any()) .Returns(Task.FromResult(rawUpcomingEvents)); // ReSharper disable once CollectionNeverUpdated.Local @@ -116,7 +117,8 @@ public async Task GetUpcomingEventsAsync_WithLeagues_ReturnsUpcomingEvents() results.Should().NotBeNull().And.Equal(upcomingEvents); await webApiClientMock.Received() - .OddsAsync(league, secretValue, Regions.Eu, Markets.H2h, DateFormat.Iso, OddsFormat.Decimal, null, null, token) + .OddsAsync(league, secretValue, Regions.Eu, Markets.H2h, DateFormat.Iso, OddsFormat.Decimal, null, null, + token) .ConfigureAwait(false); var received = converterMock.ReceivedCalls().ToList(); @@ -138,9 +140,10 @@ await webApiClientMock.Received() [Test] public async Task GetEventResultsAsync_WithLeagues_ReturnsEventResults() { - ICollection rawEventResults = [new()]; + ICollection rawEventResults = [new Anonymous3()]; var webApiClientMock = Substitute.For(); - webApiClientMock.ScoresAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + webApiClientMock.ScoresAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any()) .Returns(Task.FromResult(rawEventResults)); // ReSharper disable once CollectionNeverUpdated.Local diff --git a/OddsCollector.Functions.Tests/OddsCollector.Functions.Tests.csproj b/OddsCollector.Functions.Tests/OddsCollector.Functions.Tests.csproj index c79a94e..cbb3cd0 100644 --- a/OddsCollector.Functions.Tests/OddsCollector.Functions.Tests.csproj +++ b/OddsCollector.Functions.Tests/OddsCollector.Functions.Tests.csproj @@ -10,20 +10,20 @@ - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + diff --git a/OddsCollector.Functions.Tests/Strategies/Configuration/ServiceCollectionExtensionsTests.cs b/OddsCollector.Functions.Tests/Strategies/Configuration/ServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..dcfd5ef --- /dev/null +++ b/OddsCollector.Functions.Tests/Strategies/Configuration/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using OddsCollector.Functions.Strategies; +using OddsCollector.Functions.Strategies.Configuration; + +namespace OddsCollector.Functions.Tests.Strategies.Configuration; + +[Parallelizable(ParallelScope.All)] +internal class ServiceCollectionExtensionsTests +{ + [Test] + public void AddPredictionStrategy_AddsProperlyConfiguredPredictionStrategy() + { + var services = new ServiceCollection(); + + services.AddPredictionStrategy(); + + var strategy = + services.FirstOrDefault( + x => x.ServiceType == typeof(IPredictionStrategy) + && x.ImplementationType == typeof(AdjustedConsensusStrategy) + && x.Lifetime == ServiceLifetime.Singleton); + + strategy.Should().NotBeNull(); + } +} diff --git a/OddsCollector.Functions/CommunicationServices/Configuration/EmailSenderOptions.cs b/OddsCollector.Functions/CommunicationServices/Configuration/EmailSenderOptions.cs deleted file mode 100644 index 678f640..0000000 --- a/OddsCollector.Functions/CommunicationServices/Configuration/EmailSenderOptions.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OddsCollector.Functions.CommunicationServices.Configuration; - -internal sealed class EmailSenderOptions -{ - [Required(AllowEmptyStrings = false)] public string RecipientAddress { get; set; } = string.Empty; - - [Required(AllowEmptyStrings = false)] public string SenderAddress { get; set; } = string.Empty; - - [Required(AllowEmptyStrings = false)] public string Subject { get; set; } = string.Empty; - - public void SetRecipientAddress(string? recipientAddress) - { - ArgumentException.ThrowIfNullOrEmpty(recipientAddress); - - RecipientAddress = recipientAddress; - } - - public void SetSenderAddress(string? senderAddress) - { - ArgumentException.ThrowIfNullOrEmpty(senderAddress); - - SenderAddress = senderAddress; - } - - public void SetSubject(string? subject) - { - ArgumentException.ThrowIfNullOrEmpty(subject); - - Subject = subject; - } -} diff --git a/OddsCollector.Functions/CommunicationServices/EmailSender.cs b/OddsCollector.Functions/CommunicationServices/EmailSender.cs deleted file mode 100644 index 7f5ea4a..0000000 --- a/OddsCollector.Functions/CommunicationServices/EmailSender.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Text.Json; -using Azure; -using Azure.Communication.Email; -using Microsoft.Extensions.Options; -using OddsCollector.Functions.CommunicationServices.Configuration; -using OddsCollector.Functions.Models; - -namespace OddsCollector.Functions.CommunicationServices; - -internal sealed class EmailSender(IOptions? options, EmailClient? client) : IEmailSender -{ - private readonly EmailClient _client = client ?? throw new ArgumentNullException(nameof(client)); - private readonly EmailSenderOptions _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); - private static readonly JsonSerializerOptions _serializerOptions = new() { WriteIndented = true }; - - public async Task SendEmailAsync(IEnumerable predictions, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(predictions); - - var content = JsonSerializer.Serialize(predictions, _serializerOptions); - - await _client.SendAsync( - WaitUntil.Completed, _options.SenderAddress, _options.RecipientAddress, _options.Subject, - content, cancellationToken: cancellationToken).ConfigureAwait(false); - - await Task.CompletedTask.ConfigureAwait(false); - } -} diff --git a/OddsCollector.Functions/CommunicationServices/IEmailSender.cs b/OddsCollector.Functions/CommunicationServices/IEmailSender.cs deleted file mode 100644 index a4c2363..0000000 --- a/OddsCollector.Functions/CommunicationServices/IEmailSender.cs +++ /dev/null @@ -1,8 +0,0 @@ -using OddsCollector.Functions.Models; - -namespace OddsCollector.Functions.CommunicationServices; - -internal interface IEmailSender -{ - Task SendEmailAsync(IEnumerable predictions, CancellationToken cancellationToken); -} diff --git a/OddsCollector.Functions/CosmosDb/ContainerFactory.cs b/OddsCollector.Functions/CosmosDb/ContainerFactory.cs deleted file mode 100644 index e36bbec..0000000 --- a/OddsCollector.Functions/CosmosDb/ContainerFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Azure.Cosmos; - -namespace OddsCollector.Functions.CosmosDb; - -internal static class ContainerFactory -{ - public static Container CreateContainer(string? connectionString, string? databaseId, string? containerId) - { - ArgumentException.ThrowIfNullOrEmpty(connectionString); - ArgumentException.ThrowIfNullOrEmpty(databaseId); - ArgumentException.ThrowIfNullOrEmpty(containerId); - - return new CosmosClient(connectionString).GetContainer(databaseId, containerId); - } -} diff --git a/OddsCollector.Functions/CosmosDb/CosmosDbClient.cs b/OddsCollector.Functions/CosmosDb/CosmosDbClient.cs deleted file mode 100644 index 1a66335..0000000 --- a/OddsCollector.Functions/CosmosDb/CosmosDbClient.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Azure.Cosmos; -using Microsoft.Azure.Cosmos.Linq; -using OddsCollector.Functions.Models; - -namespace OddsCollector.Functions.CosmosDb; - -internal sealed class CosmosDbClient(Container? container) : ICosmosDbClient -{ - private readonly Container _container = container ?? throw new ArgumentNullException(nameof(container)); - - public async Task> GetEventPredictionsAsync(CancellationToken cancellationToken) - { - var queryable = _container.GetItemLinqQueryable(); - - // cosmosdb doesn't support grouping - var matches = from prediction in queryable - where prediction.CommenceTime >= DateTime.UtcNow - select prediction; - - List cosmosdbresult = []; - - using FeedIterator feed = matches.ToFeedIterator(); - - while (feed.HasMoreResults) - { - var response = await feed.ReadNextAsync(cancellationToken).ConfigureAwait(false); - - cosmosdbresult.AddRange(response); - } - - // so doing it manually - var groups = cosmosdbresult.GroupBy(p => p.Id); - - return groups.Select(group => group.OrderByDescending(p => p.Timestamp).First()).ToList(); - } -} diff --git a/OddsCollector.Functions/CosmosDb/ICosmosDbClient.cs b/OddsCollector.Functions/CosmosDb/ICosmosDbClient.cs deleted file mode 100644 index 72b7fc0..0000000 --- a/OddsCollector.Functions/CosmosDb/ICosmosDbClient.cs +++ /dev/null @@ -1,8 +0,0 @@ -using OddsCollector.Functions.Models; - -namespace OddsCollector.Functions.CosmosDb; - -internal interface ICosmosDbClient -{ - Task> GetEventPredictionsAsync(CancellationToken cancellationToken); -} diff --git a/OddsCollector.Functions/Functions/EventResultsFunction.cs b/OddsCollector.Functions/Functions/EventResultsFunction.cs index 3e12146..1957bf0 100644 --- a/OddsCollector.Functions/Functions/EventResultsFunction.cs +++ b/OddsCollector.Functions/Functions/EventResultsFunction.cs @@ -1,23 +1,20 @@ -using System.Runtime.CompilerServices; -using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; using OddsCollector.Functions.Models; using OddsCollector.Functions.OddsApi; -[assembly: InternalsVisibleTo("OddsCollector.Functions.Tests")] -// DynamicProxyGenAssembly2 is a temporary assembly built by mocking systems that use CastleProxy -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] - namespace OddsCollector.Functions.Functions; -internal sealed class EventResultsFunction(ILogger? logger, IOddsApiClient? client) +internal class EventResultsFunction(ILogger? logger, IOddsApiClient? client) { private readonly IOddsApiClient _client = client ?? throw new ArgumentNullException(nameof(client)); private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); [Function(nameof(EventResultsFunction))] [CosmosDBOutput("%CosmosDb:Database%", "%CosmosDb:EventResultsContainer%", Connection = "CosmosDb:Connection")] - public async Task Run([TimerTrigger("%EventResultsFunction:TimerInterval%")] CancellationToken cancellationToken) + public async Task Run( + [TimerTrigger("%EventResultsFunction:TimerInterval%")] + CancellationToken cancellationToken) { try { diff --git a/OddsCollector.Functions/Functions/NotificationFunction.cs b/OddsCollector.Functions/Functions/NotificationFunction.cs deleted file mode 100644 index ce7634a..0000000 --- a/OddsCollector.Functions/Functions/NotificationFunction.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.Azure.Functions.Worker; -using Microsoft.Extensions.Logging; -using OddsCollector.Functions.CommunicationServices; -using OddsCollector.Functions.CosmosDb; - -namespace OddsCollector.Functions.Functions; - -internal sealed class NotificationFunction(ILogger? logger, IEmailSender? sender, ICosmosDbClient? client) -{ - private readonly ICosmosDbClient _client = client ?? throw new ArgumentNullException(nameof(client)); - private readonly IEmailSender _sender = sender ?? throw new ArgumentNullException(nameof(sender)); - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - [Function(nameof(NotificationFunction))] - public async Task Run([TimerTrigger("%NotificationFunction:TimerInterval%")] CancellationToken cancellationToken) - { - try - { - var predictions = await _client.GetEventPredictionsAsync(cancellationToken).ConfigureAwait(false); - await _sender.SendEmailAsync(predictions, cancellationToken).ConfigureAwait(false); - } - catch (Exception exception) - { - _logger.LogError(exception, "Failed to send e-mail notification"); - } - } -} diff --git a/OddsCollector.Functions/Functions/PredictionFunction.cs b/OddsCollector.Functions/Functions/PredictionFunction.cs index 276c877..d9ec1a9 100644 --- a/OddsCollector.Functions/Functions/PredictionFunction.cs +++ b/OddsCollector.Functions/Functions/PredictionFunction.cs @@ -6,32 +6,33 @@ namespace OddsCollector.Functions.Functions; -internal sealed class PredictionFunction(ILogger? logger, IPredictionStrategy? strategy) +internal class PredictionFunction(ILogger? logger, IPredictionStrategy? strategy) { - private readonly IPredictionStrategy _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + private readonly IPredictionStrategy _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); [Function(nameof(PredictionFunction))] [CosmosDBOutput("%CosmosDb:Database%", "%CosmosDb:EventPredictionsContainer%", Connection = "CosmosDb:Connection")] public async Task Run( [ServiceBusTrigger("%ServiceBus:Queue%", Connection = "ServiceBus:Connection", IsBatched = true)] - ServiceBusReceivedMessage[] messages, ServiceBusMessageActions messageActions, CancellationToken cancellationToken) + ServiceBusReceivedMessage[] messages, ServiceBusMessageActions messageActions, + CancellationToken cancellationToken) { List predictions = []; - for (var i = 0; i < messages.Length; i++) + foreach (var message in messages) { try { - var upcomingEvent = messages[i].Body.ToObjectFromJson(); + var upcomingEvent = message.Body.ToObjectFromJson(); predictions.Add(_strategy.GetPrediction(upcomingEvent, DateTime.UtcNow)); - await messageActions.CompleteMessageAsync(messages[i], cancellationToken).ConfigureAwait(false); + await messageActions.CompleteMessageAsync(message, cancellationToken).ConfigureAwait(false); } catch (Exception exception) { - _logger.LogError(exception, "Failed to convert message with id {Id}", messages[i].MessageId); + _logger.LogError(exception, "Failed to convert message with id {Id}", message.MessageId); } } diff --git a/OddsCollector.Functions/Functions/PredictionsHttpFunction.cs b/OddsCollector.Functions/Functions/PredictionsHttpFunction.cs new file mode 100644 index 0000000..8f5cef8 --- /dev/null +++ b/OddsCollector.Functions/Functions/PredictionsHttpFunction.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Text.Json; +using Microsoft.Azure.Functions.Worker; +using Microsoft.Azure.Functions.Worker.Http; +using Microsoft.Extensions.Logging; +using OddsCollector.Functions.Models; + +namespace OddsCollector.Functions.Functions; + +internal class PredictionsHttpFunction(ILogger logger) +{ + private static readonly JsonSerializerOptions SerializerOptions = new() { WriteIndented = true }; + + private readonly ILogger _logger = + logger ?? throw new ArgumentNullException(nameof(logger)); + + [Function(nameof(PredictionsHttpFunction))] + public HttpResponseData Run( + [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] + HttpRequestData request, + [CosmosDBInput( + "%CosmosDb:Database%", + "%CosmosDb:EventPredictionsContainer%", + Connection = "CosmosDb:Connection", + SqlQuery = "SELECT * FROM p WHERE p.CommenceTime > GetCurrentDateTime()")] + EventPrediction[] predictions) + { + try + { + // cosmosdb sql doesn't support grouping + // so doing it manually + var grouped = predictions.GroupBy(p => p.Id) + .Select(group => group.OrderByDescending(p => p.Timestamp).First()).ToList(); + + var serialized = JsonSerializer.Serialize(grouped, SerializerOptions); + + return CreateResponse(HttpStatusCode.OK, request, serialized); + } + catch (Exception exception) + { + const string message = "Failed to return predictions"; + + _logger.LogError(exception, message); + + return CreateResponse(HttpStatusCode.InternalServerError, request, message); + } + } + + private static HttpResponseData CreateResponse(HttpStatusCode code, HttpRequestData request, string body) + { + var response = request.CreateResponse(code); + response.Headers.Add("Content-Type", "text/plain; charset=utf-8"); + response.WriteString(body); + + return response; + } +} diff --git a/OddsCollector.Functions/Functions/UpcomingEventsFunction.cs b/OddsCollector.Functions/Functions/UpcomingEventsFunction.cs index 4828ec3..d097c54 100644 --- a/OddsCollector.Functions/Functions/UpcomingEventsFunction.cs +++ b/OddsCollector.Functions/Functions/UpcomingEventsFunction.cs @@ -5,14 +5,18 @@ namespace OddsCollector.Functions.Functions; -internal sealed class UpcomingEventsFunction(ILogger? logger, IOddsApiClient? client) +internal class UpcomingEventsFunction(ILogger? logger, IOddsApiClient? client) { private readonly IOddsApiClient _client = client ?? throw new ArgumentNullException(nameof(client)); - private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + private readonly ILogger + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); [Function(nameof(UpcomingEventsFunction))] [ServiceBusOutput("%ServiceBus:Queue%", Connection = "ServiceBus:Connection")] - public async Task Run([TimerTrigger("%UpcomingEventsFunction:TimerInterval%")] CancellationToken cancellationToken) + public async Task Run( + [TimerTrigger("%UpcomingEventsFunction:TimerInterval%")] + CancellationToken cancellationToken) { try { diff --git a/OddsCollector.Functions/Models/Constants.cs b/OddsCollector.Functions/Models/Constants.cs index 89e062e..a03fae4 100644 --- a/OddsCollector.Functions/Models/Constants.cs +++ b/OddsCollector.Functions/Models/Constants.cs @@ -1,6 +1,6 @@ namespace OddsCollector.Functions.Models; -public static class Constants +internal static class Constants { public const string Draw = "Draw"; } diff --git a/OddsCollector.Functions/Models/EventPrediction.cs b/OddsCollector.Functions/Models/EventPrediction.cs index fa1d817..4c8e514 100644 --- a/OddsCollector.Functions/Models/EventPrediction.cs +++ b/OddsCollector.Functions/Models/EventPrediction.cs @@ -2,7 +2,7 @@ namespace OddsCollector.Functions.Models; -public class EventPrediction +internal class EventPrediction { // duplicating information to avoid complex cosmosdb queries public string AwayTeam { get; set; } = string.Empty; diff --git a/OddsCollector.Functions/Models/EventPredictionBuilder.cs b/OddsCollector.Functions/Models/EventPredictionBuilder.cs index 72c44c4..445ec7b 100644 --- a/OddsCollector.Functions/Models/EventPredictionBuilder.cs +++ b/OddsCollector.Functions/Models/EventPredictionBuilder.cs @@ -1,6 +1,6 @@ namespace OddsCollector.Functions.Models; -public class EventPredictionBuilder +internal class EventPredictionBuilder { public EventPrediction Instance { get; } = new(); diff --git a/OddsCollector.Functions/Models/EventResult.cs b/OddsCollector.Functions/Models/EventResult.cs index 6cd74dc..cf4ca8e 100644 --- a/OddsCollector.Functions/Models/EventResult.cs +++ b/OddsCollector.Functions/Models/EventResult.cs @@ -2,7 +2,7 @@ namespace OddsCollector.Functions.Models; -public class EventResult +internal class EventResult { public DateTime CommenceTime { get; set; } = DateTime.MinValue; diff --git a/OddsCollector.Functions/Models/EventResultBuilder.cs b/OddsCollector.Functions/Models/EventResultBuilder.cs index 70fed45..baafa40 100644 --- a/OddsCollector.Functions/Models/EventResultBuilder.cs +++ b/OddsCollector.Functions/Models/EventResultBuilder.cs @@ -1,6 +1,6 @@ namespace OddsCollector.Functions.Models; -public class EventResultBuilder +internal class EventResultBuilder { public EventResult Instance { get; } = new(); diff --git a/OddsCollector.Functions/Models/Odd.cs b/OddsCollector.Functions/Models/Odd.cs index 44dd6ce..fbd39e1 100644 --- a/OddsCollector.Functions/Models/Odd.cs +++ b/OddsCollector.Functions/Models/Odd.cs @@ -1,6 +1,6 @@ namespace OddsCollector.Functions.Models; -public class Odd +internal class Odd { public double Away { get; set; } public string Bookmaker { get; set; } = string.Empty; diff --git a/OddsCollector.Functions/Models/OddBuilder.cs b/OddsCollector.Functions/Models/OddBuilder.cs index cab4128..3907a55 100644 --- a/OddsCollector.Functions/Models/OddBuilder.cs +++ b/OddsCollector.Functions/Models/OddBuilder.cs @@ -1,6 +1,6 @@ namespace OddsCollector.Functions.Models; -public class OddBuilder +internal class OddBuilder { public Odd Instance { get; } = new(); diff --git a/OddsCollector.Functions/Models/UpcomingEvent.cs b/OddsCollector.Functions/Models/UpcomingEvent.cs index d5b2ae8..8c35dbc 100644 --- a/OddsCollector.Functions/Models/UpcomingEvent.cs +++ b/OddsCollector.Functions/Models/UpcomingEvent.cs @@ -1,6 +1,6 @@ namespace OddsCollector.Functions.Models; -public class UpcomingEvent +internal class UpcomingEvent { public string AwayTeam { get; set; } = string.Empty; public DateTime CommenceTime { get; set; } = DateTime.MinValue; diff --git a/OddsCollector.Functions/Models/UpcomingEventBuilder.cs b/OddsCollector.Functions/Models/UpcomingEventBuilder.cs index 632d41e..835c960 100644 --- a/OddsCollector.Functions/Models/UpcomingEventBuilder.cs +++ b/OddsCollector.Functions/Models/UpcomingEventBuilder.cs @@ -1,6 +1,6 @@ namespace OddsCollector.Functions.Models; -public class UpcomingEventBuilder +internal class UpcomingEventBuilder { public UpcomingEvent Instance { get; } = new(); diff --git a/OddsCollector.Functions/OddsApi/Configuration/OddsApiClientOptions.cs b/OddsCollector.Functions/OddsApi/Configuration/OddsApiClientOptions.cs index 2a7c2a1..e9c07f8 100644 --- a/OddsCollector.Functions/OddsApi/Configuration/OddsApiClientOptions.cs +++ b/OddsCollector.Functions/OddsApi/Configuration/OddsApiClientOptions.cs @@ -1,6 +1,6 @@ namespace OddsCollector.Functions.OddsApi.Configuration; -public class OddsApiClientOptions +internal class OddsApiClientOptions { public HashSet Leagues { get; init; } = []; diff --git a/OddsCollector.Functions/OddsApi/Configuration/ServiceCollectionExtensions.cs b/OddsCollector.Functions/OddsApi/Configuration/ServiceCollectionExtensions.cs index af44a6e..63af257 100644 --- a/OddsCollector.Functions/OddsApi/Configuration/ServiceCollectionExtensions.cs +++ b/OddsCollector.Functions/OddsApi/Configuration/ServiceCollectionExtensions.cs @@ -4,7 +4,7 @@ namespace OddsCollector.Functions.OddsApi.Configuration; -public static class ServiceCollectionExtensions +internal static class ServiceCollectionExtensions { public static IServiceCollection AddOddsApiClientWithDependencies(this IServiceCollection services) { diff --git a/OddsCollector.Functions/OddsApi/Converter/IOddsApiObjectConverter.cs b/OddsCollector.Functions/OddsApi/Converter/IOddsApiObjectConverter.cs index 2182106..3f53dfa 100644 --- a/OddsCollector.Functions/OddsApi/Converter/IOddsApiObjectConverter.cs +++ b/OddsCollector.Functions/OddsApi/Converter/IOddsApiObjectConverter.cs @@ -3,7 +3,7 @@ namespace OddsCollector.Functions.OddsApi.Converter; -public interface IOddsApiObjectConverter +internal interface IOddsApiObjectConverter { IEnumerable ToUpcomingEvents(ICollection? events, Guid traceId, DateTime timestamp); IEnumerable ToEventResults(ICollection? events, Guid traceId, DateTime timestamp); diff --git a/OddsCollector.Functions/OddsApi/Converter/OddsApiObjectConverter.cs b/OddsCollector.Functions/OddsApi/Converter/OddsApiObjectConverter.cs index 7535207..1afed7b 100644 --- a/OddsCollector.Functions/OddsApi/Converter/OddsApiObjectConverter.cs +++ b/OddsCollector.Functions/OddsApi/Converter/OddsApiObjectConverter.cs @@ -4,7 +4,7 @@ namespace OddsCollector.Functions.OddsApi.Converter; -public class OddsApiObjectConverter : IOddsApiObjectConverter +internal class OddsApiObjectConverter : IOddsApiObjectConverter { private const Markets2Key HeadToHeadMarketKey = Markets2Key.H2h; diff --git a/OddsCollector.Functions/OddsApi/IOddsApiClient.cs b/OddsCollector.Functions/OddsApi/IOddsApiClient.cs index 4ef4657..939888f 100644 --- a/OddsCollector.Functions/OddsApi/IOddsApiClient.cs +++ b/OddsCollector.Functions/OddsApi/IOddsApiClient.cs @@ -2,8 +2,11 @@ namespace OddsCollector.Functions.OddsApi; -public interface IOddsApiClient +internal interface IOddsApiClient { - Task> GetUpcomingEventsAsync(Guid traceId, DateTime timestamp, CancellationToken cancellationToken); - Task> GetEventResultsAsync(Guid traceId, DateTime timestamp, CancellationToken cancellationToken); + Task> GetUpcomingEventsAsync(Guid traceId, DateTime timestamp, + CancellationToken cancellationToken); + + Task> GetEventResultsAsync(Guid traceId, DateTime timestamp, + CancellationToken cancellationToken); } diff --git a/OddsCollector.Functions/OddsApi/OddsApiClient.cs b/OddsCollector.Functions/OddsApi/OddsApiClient.cs index 939ecaf..b727df9 100644 --- a/OddsCollector.Functions/OddsApi/OddsApiClient.cs +++ b/OddsCollector.Functions/OddsApi/OddsApiClient.cs @@ -6,7 +6,9 @@ namespace OddsCollector.Functions.OddsApi; -public class OddsApiClient(IOptions? options, IClient? webApiClient, +internal class OddsApiClient( + IOptions? options, + IClient? webApiClient, IOddsApiObjectConverter? objectConverter) : IOddsApiClient { private const DateFormat IsoDateFormat = DateFormat.Iso; @@ -14,18 +16,23 @@ public class OddsApiClient(IOptions? options, IClient? web private const OddsFormat DecimalOddsFormat = OddsFormat.Decimal; private const Regions EuropeanRegion = Regions.Eu; private const int DaysFromToday = 3; - private readonly IOddsApiObjectConverter _objectConverter = objectConverter ?? throw new ArgumentNullException(nameof(objectConverter)); + + private readonly IOddsApiObjectConverter _objectConverter = + objectConverter ?? throw new ArgumentNullException(nameof(objectConverter)); + private readonly OddsApiClientOptions _options = options?.Value ?? throw new ArgumentNullException(nameof(options)); private readonly IClient _webApiClient = webApiClient ?? throw new ArgumentNullException(nameof(webApiClient)); - public async Task> GetUpcomingEventsAsync(Guid traceId, DateTime timestamp, CancellationToken cancellationToken) + public async Task> GetUpcomingEventsAsync(Guid traceId, DateTime timestamp, + CancellationToken cancellationToken) { List result = []; foreach (var league in _options.Leagues) { var events = await _webApiClient.OddsAsync(league, _options.ApiKey, - EuropeanRegion, HeadToHeadMarket, IsoDateFormat, DecimalOddsFormat, null, null, cancellationToken).ConfigureAwait(false); + EuropeanRegion, HeadToHeadMarket, IsoDateFormat, DecimalOddsFormat, null, null, cancellationToken) + .ConfigureAwait(false); result.AddRange(_objectConverter.ToUpcomingEvents(events, traceId, timestamp)); } @@ -33,14 +40,16 @@ public async Task> GetUpcomingEventsAsync(Guid traceI return result; } - public async Task> GetEventResultsAsync(Guid traceId, DateTime timestamp, CancellationToken cancellationToken) + public async Task> GetEventResultsAsync(Guid traceId, DateTime timestamp, + CancellationToken cancellationToken) { List result = []; foreach (var league in _options.Leagues) { var results = - await _webApiClient.ScoresAsync(league, _options.ApiKey, DaysFromToday, cancellationToken).ConfigureAwait(false); + await _webApiClient.ScoresAsync(league, _options.ApiKey, DaysFromToday, cancellationToken) + .ConfigureAwait(false); result.AddRange(_objectConverter.ToEventResults(results, traceId, timestamp)); } diff --git a/OddsCollector.Functions/OddsApi/WebApi/parameters.nswag b/OddsCollector.Functions/OddsApi/WebApi/parameters.nswag index 188d8e1..2e1e102 100644 --- a/OddsCollector.Functions/OddsApi/WebApi/parameters.nswag +++ b/OddsCollector.Functions/OddsApi/WebApi/parameters.nswag @@ -30,8 +30,8 @@ "generateSyncMethods": false, "generatePrepareRequestAndProcessResponseAsAsyncMethods": false, "exposeJsonSerializerSettings": false, - "clientClassAccessModifier": "public", - "typeAccessModifier": "public", + "clientClassAccessModifier": "internal", + "typeAccessModifier": "internal", "generateContractsOutput": false, "contractsNamespace": null, "contractsOutputFilePath": null, diff --git a/OddsCollector.Functions/OddsCollector.Functions.csproj b/OddsCollector.Functions/OddsCollector.Functions.csproj index d46b944..e2907dc 100644 --- a/OddsCollector.Functions/OddsCollector.Functions.csproj +++ b/OddsCollector.Functions/OddsCollector.Functions.csproj @@ -7,16 +7,15 @@ enable - - - - - - - - - - + + + + + + + + + @@ -32,6 +31,6 @@ - + \ No newline at end of file diff --git a/OddsCollector.Functions/Program.cs b/OddsCollector.Functions/Program.cs index 94f89a0..3787386 100644 --- a/OddsCollector.Functions/Program.cs +++ b/OddsCollector.Functions/Program.cs @@ -1,42 +1,31 @@ -using Azure.Communication.Email; +using System.Runtime.CompilerServices; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using OddsCollector.Functions.CommunicationServices; -using OddsCollector.Functions.CommunicationServices.Configuration; -using OddsCollector.Functions.CosmosDb; using OddsCollector.Functions.OddsApi.Configuration; -using OddsCollector.Functions.Strategies; +using OddsCollector.Functions.Strategies.Configuration; -var host = new HostBuilder() - .ConfigureFunctionsWorkerDefaults() - .ConfigureServices(services => +[assembly: InternalsVisibleTo("OddsCollector.Functions.Tests")] +// DynamicProxyGenAssembly2 is a temporary assembly built by mocking systems that use CastleProxy +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] + +namespace OddsCollector.Functions; + +internal static class Program +{ + private static void Main() { - services.Configure(o => - { - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 - o.SetSubject(Environment.GetEnvironmentVariable("EmailSender:Subject")); - o.SetRecipientAddress(Environment.GetEnvironmentVariable("EmailSender:RecipientAddress")); - o.SetSenderAddress(Environment.GetEnvironmentVariable("EmailSender:SenderAddress")); - }); - services.AddSingleton( - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 - new EmailClient(Environment.GetEnvironmentVariable("EmailSender:Connection")) - ); - services.AddSingleton( - ContainerFactory.CreateContainer( - // workaround for https://github.com/MicrosoftDocs/azure-docs/issues/32962 - Environment.GetEnvironmentVariable("CosmosDb:Connection"), - Environment.GetEnvironmentVariable("CosmosDb:Database"), - Environment.GetEnvironmentVariable("CosmosDb:EventPredictionsContainer") - )); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddOddsApiClientWithDependencies(); - services.AddApplicationInsightsTelemetryWorkerService(); - services.ConfigureFunctionsApplicationInsights(); - }) - .Build(); + var host = new HostBuilder() + .ConfigureFunctionsWorkerDefaults() + .ConfigureServices(services => + { + services.AddPredictionStrategy(); + services.AddOddsApiClientWithDependencies(); + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + }) + .Build(); -host.Run(); + host.Run(); + } +} diff --git a/OddsCollector.Functions/Strategies/AdjustedConsensusStrategy.cs b/OddsCollector.Functions/Strategies/AdjustedConsensusStrategy.cs index 41a1f83..4bd433d 100644 --- a/OddsCollector.Functions/Strategies/AdjustedConsensusStrategy.cs +++ b/OddsCollector.Functions/Strategies/AdjustedConsensusStrategy.cs @@ -40,10 +40,11 @@ private static StrategyScore GetWinner(IEnumerable odds, string awayTeam, s ArgumentException.ThrowIfNullOrEmpty(awayTeam); ArgumentException.ThrowIfNullOrEmpty(homeTeam); - List scores = [ - new() { Name = Constants.Draw, Odd = CalculateAdjustedScore(enumerated, Draw, 0.057) }, - new() { Name = awayTeam, Odd = CalculateAdjustedScore(enumerated, AwayTeamWins, 0.034) }, - new() { Name = homeTeam, Odd = CalculateAdjustedScore(enumerated, HomeTeamWins, 0.037) } + List scores = + [ + new StrategyScore { Name = Constants.Draw, Odd = CalculateAdjustedScore(enumerated, Draw, 0.057) }, + new StrategyScore { Name = awayTeam, Odd = CalculateAdjustedScore(enumerated, AwayTeamWins, 0.034) }, + new StrategyScore { Name = homeTeam, Odd = CalculateAdjustedScore(enumerated, HomeTeamWins, 0.037) } ]; var winner = scores.MaxBy(p => p.Odd); @@ -56,7 +57,7 @@ private static StrategyScore GetWinner(IEnumerable odds, string awayTeam, s private static double CalculateAdjustedScore(IEnumerable odds, Func filter, double adjustment) { var average = odds.Select(filter).Average(); - return average == 0 ? 0 : 1 / average - adjustment; + return average == 0 ? 0 : (1 / average) - adjustment; } private static Odd GetBestOdd(IEnumerable odds, string winner, string awayTeam, string homeTeam) diff --git a/OddsCollector.Functions/Strategies/Configuration/ServiceCollectionExtensions.cs b/OddsCollector.Functions/Strategies/Configuration/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..78a3ccb --- /dev/null +++ b/OddsCollector.Functions/Strategies/Configuration/ServiceCollectionExtensions.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace OddsCollector.Functions.Strategies.Configuration; + +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection AddPredictionStrategy(this IServiceCollection services) + { + services.AddSingleton(); + + return services; + } +} diff --git a/OddsCollector.Functions/host.json b/OddsCollector.Functions/host.json index ee5cf5f..5df170b 100644 --- a/OddsCollector.Functions/host.json +++ b/OddsCollector.Functions/host.json @@ -1,12 +1,12 @@ { - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - }, - "enableLiveMetricsFilters": true - } + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true } + } } \ No newline at end of file