From 724bdd7f19fe3a661d68ae3c07639382ba4fcb87 Mon Sep 17 00:00:00 2001 From: Corey Kunkle Date: Fri, 14 Nov 2025 11:57:05 -0500 Subject: [PATCH 1/3] feat: Added initial projects structure for api and tests --- .gitignore | 8 ++++++ .../InvestmentPerformance.Tests.csproj | 26 +++++++++++++++++++ .../.idea/.gitignore | 15 +++++++++++ .../.idea/encodings.xml | 4 +++ .../.idea/indexLayout.xml | 8 ++++++ .../.idea.InvestmentPerformance/.idea/vcs.xml | 6 +++++ .../InvestmentPerformance.Api.csproj | 15 +++++++++++ .../InvestmentPerformance.Api.http | 6 +++++ .../InvestmentPerformance.sln | 22 ++++++++++++++++ InvestmentPerformance/Program.cs | 24 +++++++++++++++++ .../Properties/launchSettings.json | 25 ++++++++++++++++++ InvestmentPerformance/Readme.md | 22 ++++++++++++++++ .../appsettings.Development.json | 8 ++++++ InvestmentPerformance/appsettings.json | 9 +++++++ 14 files changed, 198 insertions(+) create mode 100644 .gitignore create mode 100644 InvestmentPerformance.Tests/InvestmentPerformance.Tests.csproj create mode 100644 InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/.gitignore create mode 100644 InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/encodings.xml create mode 100644 InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/indexLayout.xml create mode 100644 InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/vcs.xml create mode 100644 InvestmentPerformance/InvestmentPerformance.Api.csproj create mode 100644 InvestmentPerformance/InvestmentPerformance.Api.http create mode 100644 InvestmentPerformance/InvestmentPerformance.sln create mode 100644 InvestmentPerformance/Program.cs create mode 100644 InvestmentPerformance/Properties/launchSettings.json create mode 100644 InvestmentPerformance/Readme.md create mode 100644 InvestmentPerformance/appsettings.Development.json create mode 100644 InvestmentPerformance/appsettings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..d93357a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Build +bin/ +obj/ +TestResults/ + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/InvestmentPerformance.Tests/InvestmentPerformance.Tests.csproj b/InvestmentPerformance.Tests/InvestmentPerformance.Tests.csproj new file mode 100644 index 00000000..5fb28741 --- /dev/null +++ b/InvestmentPerformance.Tests/InvestmentPerformance.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/.gitignore b/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/.gitignore new file mode 100644 index 00000000..7e7a3ba2 --- /dev/null +++ b/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/.gitignore @@ -0,0 +1,15 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/modules.xml +/projectSettingsUpdater.xml +/.idea.InvestmentPerformance.iml +/contentModel.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/encodings.xml b/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/encodings.xml new file mode 100644 index 00000000..df87cf95 --- /dev/null +++ b/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/indexLayout.xml b/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/indexLayout.xml new file mode 100644 index 00000000..7b08163c --- /dev/null +++ b/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/vcs.xml b/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/vcs.xml new file mode 100644 index 00000000..6c0b8635 --- /dev/null +++ b/InvestmentPerformance/.idea/.idea.InvestmentPerformance/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/InvestmentPerformance/InvestmentPerformance.Api.csproj b/InvestmentPerformance/InvestmentPerformance.Api.csproj new file mode 100644 index 00000000..7a359839 --- /dev/null +++ b/InvestmentPerformance/InvestmentPerformance.Api.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + enable + enable + + + + + + + + + diff --git a/InvestmentPerformance/InvestmentPerformance.Api.http b/InvestmentPerformance/InvestmentPerformance.Api.http new file mode 100644 index 00000000..2ecaf204 --- /dev/null +++ b/InvestmentPerformance/InvestmentPerformance.Api.http @@ -0,0 +1,6 @@ +@InvestmentPerformance.Api_HostAddress = http://localhost:5283 + +GET {{InvestmentPerformance.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/InvestmentPerformance/InvestmentPerformance.sln b/InvestmentPerformance/InvestmentPerformance.sln new file mode 100644 index 00000000..924c402c --- /dev/null +++ b/InvestmentPerformance/InvestmentPerformance.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformance.Api", "InvestmentPerformance.Api.csproj", "{A220351A-4C15-4345-B30F-5B4FEB4CEFAD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformance.Tests", "..\InvestmentPerformance.Tests\InvestmentPerformance.Tests.csproj", "{080578E6-F472-49B0-B7EC-9FBB5703B49B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A220351A-4C15-4345-B30F-5B4FEB4CEFAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A220351A-4C15-4345-B30F-5B4FEB4CEFAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A220351A-4C15-4345-B30F-5B4FEB4CEFAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A220351A-4C15-4345-B30F-5B4FEB4CEFAD}.Release|Any CPU.Build.0 = Release|Any CPU + {080578E6-F472-49B0-B7EC-9FBB5703B49B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {080578E6-F472-49B0-B7EC-9FBB5703B49B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {080578E6-F472-49B0-B7EC-9FBB5703B49B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {080578E6-F472-49B0-B7EC-9FBB5703B49B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/InvestmentPerformance/Program.cs b/InvestmentPerformance/Program.cs new file mode 100644 index 00000000..a2f9267a --- /dev/null +++ b/InvestmentPerformance/Program.cs @@ -0,0 +1,24 @@ +using InvestmentPerformance.Api.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var builder = WebApplication.CreateBuilder(args); + +//Configure services +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.MapControllers(); +app.Run(); diff --git a/InvestmentPerformance/Properties/launchSettings.json b/InvestmentPerformance/Properties/launchSettings.json new file mode 100644 index 00000000..159e0314 --- /dev/null +++ b/InvestmentPerformance/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7061;http://localhost:5283", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/InvestmentPerformance/Readme.md b/InvestmentPerformance/Readme.md new file mode 100644 index 00000000..94e396b5 --- /dev/null +++ b/InvestmentPerformance/Readme.md @@ -0,0 +1,22 @@ +# How to Run +### With IDE + + - Open the solution file in an IDE and run the InvestmentPerformance.Api project + - You may need to run NuGet package restore + - Swagger should open and be able to test the api functionality + +### Without IDE + + - Open a command prompt window + - Navigate to the directory of the project and run ```dotnet build InvestmentPerformance.Api.csproj``` + - Navigate into the project folder and run ```dotnet run``` + - Open a web browser and navigate to [Swagger](htttp://localhost:5283/swagger) + + + # Assumptions + + - I've assumed since the project will be part of a larger system the database object in SQLServer exists for an Investment, I have mocked the data in the service and created my model according to that. + + - Assuming data coming in to be validated before it was committed to the DB. + + - I opted not to create a front end interface for this feature since the "User stories" were about creating an API and I felt it would be out of scope and the front end would be a part of the larger system. \ No newline at end of file diff --git a/InvestmentPerformance/appsettings.Development.json b/InvestmentPerformance/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/InvestmentPerformance/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/InvestmentPerformance/appsettings.json b/InvestmentPerformance/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/InvestmentPerformance/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} From 8d90d4838371b66c305b5f39ad5cd864fa5807d9 Mon Sep 17 00:00:00 2001 From: Corey Kunkle Date: Fri, 14 Nov 2025 14:13:44 -0500 Subject: [PATCH 2/3] feat: Added Services and Models for Investments --- .../InvestmentServiceTests.cs | 30 +++++++++++++++ InvestmentPerformance/Program.cs | 1 + .../src/Models/InvestmentModel.cs | 37 +++++++++++++++++++ InvestmentPerformance/src/Models/StockTerm.cs | 7 ++++ .../src/Services/IInvestmentService.cs | 24 ++++++++++++ .../src/Services/InvestmentService.cs | 22 +++++++++++ 6 files changed, 121 insertions(+) create mode 100644 InvestmentPerformance.Tests/InvestmentServiceTests.cs create mode 100644 InvestmentPerformance/src/Models/InvestmentModel.cs create mode 100644 InvestmentPerformance/src/Models/StockTerm.cs create mode 100644 InvestmentPerformance/src/Services/IInvestmentService.cs create mode 100644 InvestmentPerformance/src/Services/InvestmentService.cs diff --git a/InvestmentPerformance.Tests/InvestmentServiceTests.cs b/InvestmentPerformance.Tests/InvestmentServiceTests.cs new file mode 100644 index 00000000..09271839 --- /dev/null +++ b/InvestmentPerformance.Tests/InvestmentServiceTests.cs @@ -0,0 +1,30 @@ +using InvestmentPerformance.Api.Services; +using Xunit; + +namespace InvestmentPerformance.Tests; + +public class InvestmentServiceTests +{ + [Fact] + public void GetInvestmentsByUserId_GivenInvalidUserId_ReturnsNothing() + { + const int userId = -1; + var investmentService = new InvestmentService(); + + var result = investmentService.GetInvestmentsByUserId(userId); + + Assert.Empty(result); + } + + [Fact] + public void GetInvestments_GivenInvalidInvestmentId_ReturnsNull() + { + const int investmentId = -1; + var investmentService = new InvestmentService(); + + var result = investmentService.GetInvestmentById(investmentId); + + Assert.Null(result); + } + +} \ No newline at end of file diff --git a/InvestmentPerformance/Program.cs b/InvestmentPerformance/Program.cs index a2f9267a..2d8cfe5f 100644 --- a/InvestmentPerformance/Program.cs +++ b/InvestmentPerformance/Program.cs @@ -9,6 +9,7 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); +builder.Services.AddScoped(); var app = builder.Build(); diff --git a/InvestmentPerformance/src/Models/InvestmentModel.cs b/InvestmentPerformance/src/Models/InvestmentModel.cs new file mode 100644 index 00000000..192decc2 --- /dev/null +++ b/InvestmentPerformance/src/Models/InvestmentModel.cs @@ -0,0 +1,37 @@ +using System; +using InvestmentPerformance.Api.Services; + +namespace InvestmentPerformance.Api.Models; + +public class InvestmentModel( + int id, + int userId, + string name, + decimal numShares, + decimal costPerShare, + decimal currentPrice, + DateTime purchaseDate) +{ + public int Id { get; set; } = id; + public int UserId { get; set; } = userId; + public string Name { get; set; } = name; + public decimal NumberOfShares { get; set; } = numShares; + public decimal CostPerShare { get; set; } = costPerShare; + public decimal CurrentPrice { get; set; } = currentPrice; + public DateTime PurchaseDate { get; set; } = purchaseDate; + + public InvestmentRecord ToInvestment() + { + var currentValue = NumberOfShares * CurrentPrice; + + return new InvestmentRecord( + Id, + NumberOfShares, + CostPerShare, + currentValue, + CurrentPrice, + DateTime.UtcNow.Date.Subtract(PurchaseDate).Days > 365 ? StockTerm.LongTerm : StockTerm.ShortTerm, + currentValue - CostPerShare * NumberOfShares + ); + } +} \ No newline at end of file diff --git a/InvestmentPerformance/src/Models/StockTerm.cs b/InvestmentPerformance/src/Models/StockTerm.cs new file mode 100644 index 00000000..729f1255 --- /dev/null +++ b/InvestmentPerformance/src/Models/StockTerm.cs @@ -0,0 +1,7 @@ +namespace InvestmentPerformance.Api.Models; + +public enum StockTerm +{ + ShortTerm = 0, + LongTerm = 1, +} \ No newline at end of file diff --git a/InvestmentPerformance/src/Services/IInvestmentService.cs b/InvestmentPerformance/src/Services/IInvestmentService.cs new file mode 100644 index 00000000..ac6a4b87 --- /dev/null +++ b/InvestmentPerformance/src/Services/IInvestmentService.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using InvestmentPerformance.Api.Models; + +namespace InvestmentPerformance.Api.Services; + +public interface IInvestmentService +{ + IEnumerable GetInvestmentsByUserId(int userId); + InvestmentRecord? GetInvestmentById(int investmentId); +} + +public record InvestmentRecord( + int Id, + decimal NumberOfShares, + decimal CostPerShare, + decimal CurrentValue, + decimal CurrentPrice, + StockTerm Term, + decimal TotalGainLoss); + +public record UserInvestmentRecord( + int UserId, + string Name +); \ No newline at end of file diff --git a/InvestmentPerformance/src/Services/InvestmentService.cs b/InvestmentPerformance/src/Services/InvestmentService.cs new file mode 100644 index 00000000..b9edbce8 --- /dev/null +++ b/InvestmentPerformance/src/Services/InvestmentService.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using InvestmentPerformance.Api.Models; + +namespace InvestmentPerformance.Api.Services; + +public class InvestmentService : IInvestmentService +{ + private readonly List _data = + [ + new(1, 101, "Ford Motor Company", 3, 13.25m, 13.25m, DateTime.UtcNow), + new(2, 102, "Nvidia Corporation", 5, 102.67m, 189.06m, DateTime.UtcNow.AddDays(-10)), + new(3, 102, "SoFi Technologies", 10, 10.50m, 28.38m, DateTime.UtcNow.AddYears(-2)), + new(4, 102, "SoFi Technologies", 2.5m, 28.38m, 28.38m, DateTime.UtcNow) + ]; + + public IEnumerable GetInvestmentsByUserId(int userId) => + _data.Where(d => d.UserId == userId).Select(i => new UserInvestmentRecord(i.Id, i.Name)); + + public InvestmentRecord? GetInvestmentById(int investmentId) => _data.FirstOrDefault(i => i.Id == investmentId)?.ToInvestment(); +} \ No newline at end of file From ef724d5142ce3df33fc23aeb0b71b7e728248a45 Mon Sep 17 00:00:00 2001 From: Corey Kunkle Date: Fri, 14 Nov 2025 14:24:21 -0500 Subject: [PATCH 3/3] feat: Added API controller and tests --- .../InvestmentAPITests.cs | 54 +++++++++++++++ .../src/Controllers/InvestmentController.cs | 65 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 InvestmentPerformance.Tests/InvestmentAPITests.cs create mode 100644 InvestmentPerformance/src/Controllers/InvestmentController.cs diff --git a/InvestmentPerformance.Tests/InvestmentAPITests.cs b/InvestmentPerformance.Tests/InvestmentAPITests.cs new file mode 100644 index 00000000..09c77b2b --- /dev/null +++ b/InvestmentPerformance.Tests/InvestmentAPITests.cs @@ -0,0 +1,54 @@ +using InvestmentPerformance.Api.Controllers; +using InvestmentPerformance.Api.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace InvestmentPerformance.Tests; + +public class InvestmentApiTests +{ + private const int ValidUserId = 101; + private const int ValidInvestmentId = 1; + private const int InvalidInvestmentId = -1; + + [Fact] + public void GetUserInvestments_GivenValidUser_ReturnsOk() + { + var investmentService = new Mock(); + var logger = new Mock>(); + var investmentController = new InvestmentController(investmentService.Object, logger.Object); + + var result = investmentController.GetUserInvestments(ValidUserId.ToString()); + + Assert.IsType(result.Result); + + } + + [Fact] + public void GetInvestment_GivenValidInvestment_ReturnsOk() + { + var investmentService = new Mock(); + var logger = new Mock>(); + var investmentController = new InvestmentController(investmentService.Object, logger.Object); + + var result = investmentController.GetInvestment(ValidInvestmentId.ToString()); + + Assert.IsType(result); + + } + + [Fact] + public void GetInvestment_GivenInvalidInvestment_ReturnsNotFound() + { + var investmentService = new Mock(); + var logger = new Mock>(); + var investmentController = new InvestmentController(investmentService.Object, logger.Object); + + var result = investmentController.GetInvestment(InvalidInvestmentId.ToString()); + + Assert.IsType(result); + } + +} \ No newline at end of file diff --git a/InvestmentPerformance/src/Controllers/InvestmentController.cs b/InvestmentPerformance/src/Controllers/InvestmentController.cs new file mode 100644 index 00000000..573fb41d --- /dev/null +++ b/InvestmentPerformance/src/Controllers/InvestmentController.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using InvestmentPerformance.Api.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace InvestmentPerformance.Api.Controllers; + +[ApiController] +[Route("investments")] +public sealed class InvestmentController(IInvestmentService investmentService, ILogger logger) + : ControllerBase +{ + /// + /// Retrieves a list of investments for a given user + /// + /// Id of the user you want investment data for + /// List of UserInvestment record for the user + [HttpGet("users/{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult> GetUserInvestments(string id) + { + try + { + var result = investmentService.GetInvestmentsByUserId(int.Parse(id)); + + return Ok(result); + } + catch(Exception ex) + { + logger.LogError(ex, ex.Message); + return BadRequest(); + } + } + + /// + /// Retrieves the information of a given investment + /// + /// Id of the investment you want information on + /// Investment record for the given investment + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public IActionResult GetInvestment(string id) + { + try + { + var result = investmentService.GetInvestmentById(int.Parse(id)); + + if(result == null) + return NotFound(); + + + return Ok(result); + } + catch (Exception ex) + { + logger.LogError(ex, ex.Message); + return BadRequest(); + } + + } +} \ No newline at end of file