From ae07aa877f89b434f77438ca4cf0fa8cc8d7c502 Mon Sep 17 00:00:00 2001 From: Erin Bazaz <50595616+bazazer@users.noreply.github.com> Date: Mon, 22 Dec 2025 07:19:22 -0500 Subject: [PATCH 1/3] adding project files --- .gitignore | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..aada8650 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/.vs +/InvestmentPerformanceWebApi/.vs/InvestmentPerformanceWebApi +/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/bin/Debug/net8.0 +/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/obj +/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/bin/Debug/net8.0 +/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/obj +/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/app.db +/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/app.db-shm +/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/app.db-wal From 1977eb5a4e52aaf32cb807b6415bb6e09b448ab3 Mon Sep 17 00:00:00 2001 From: Erin Bazaz <50595616+bazazer@users.noreply.github.com> Date: Mon, 22 Dec 2025 07:21:30 -0500 Subject: [PATCH 2/3] pushing all project files --- InvestmentPerformanceWebAPI.md | 8 +- .../ControllerTests/UserInvestmentTests.cs | 121 ++++++++++++++++++ .../DomainTests/InvestmentTests.cs | 111 ++++++++++++++++ .../InvestmentPerformanceApi.Tests.csproj | 32 +++++ .../ServiceTests/InvestmentServiceTests.cs | 100 +++++++++++++++ .../InvestmentPerformanceWebApi.sln | 36 ++++++ .../Controllers/UserInvestmentsController.cs | 84 ++++++++++++ .../Api/InvestmentPerformanceWebApi.http | 6 + .../Api/Mappings/InvestmentMappingProfile.cs | 15 +++ .../Api/Program.cs | 49 +++++++ .../Investments/InvestmentDetailView.cs | 18 +++ .../Investments/InvestmentSummaryView.cs | 8 ++ .../Api/appsettings.Development.json | 8 ++ .../Api/appsettings.json | 9 ++ .../DbInfrastructure/AppDbContext.cs | 91 +++++++++++++ .../DbInfrastructure/Models/Investment.cs | 17 +++ .../DbInfrastructure/Models/User.cs | 15 +++ .../Domain/Interfaces/IInvestmentService.cs | 10 ++ .../Domain/Models/Investment.cs | 47 +++++++ .../Domain/Services/InvestmentService.cs | 50 ++++++++ .../InvestmentPerformanceWebApi.csproj | 20 +++ .../InvestmentPerformanceWebApi.csproj.user | 6 + .../Properties/launchSettings.json | 41 ++++++ InvestmentPerformanceWebApi/README.md | 36 ++++++ 24 files changed, 936 insertions(+), 2 deletions(-) create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/ControllerTests/UserInvestmentTests.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/DomainTests/InvestmentTests.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/InvestmentPerformanceApi.Tests.csproj create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/ServiceTests/InvestmentServiceTests.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.sln create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Controllers/UserInvestmentsController.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/InvestmentPerformanceWebApi.http create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Mappings/InvestmentMappingProfile.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Program.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/ViewModels/Investments/InvestmentDetailView.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/ViewModels/Investments/InvestmentSummaryView.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/appsettings.Development.json create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/appsettings.json create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/AppDbContext.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/Models/Investment.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/Models/User.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Interfaces/IInvestmentService.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Models/Investment.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Services/InvestmentService.cs create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.csproj create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.csproj.user create mode 100644 InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Properties/launchSettings.json create mode 100644 InvestmentPerformanceWebApi/README.md diff --git a/InvestmentPerformanceWebAPI.md b/InvestmentPerformanceWebAPI.md index 2e96afac..98fec455 100644 --- a/InvestmentPerformanceWebAPI.md +++ b/InvestmentPerformanceWebAPI.md @@ -3,12 +3,16 @@ ## General instructions: Please fork this project and submit a pull request when completed. You should submit a working piece of software that is tested and can be run. We will review your pull request and execute your code, please provide instructions on how to do so. -Please keep in mind that this exercise is intended to be achievable in a couple of hours. We expect a production ready submission that demonstrates not only the code you write but quality controls such as exception handling, logging, unit tests, etc. Assume that this api will be part of a larger system. If there are larger considerations, that would have affected decisions of what is in/out of scope, please make note of your assumptions. The majority of the tech stack in use at Nuix Discover is SQLServer, C#.NET, ExtJs, Vue.js. We prefer the use of that stack, but we want you to showcase your abilities. If you don't know those specific technologies, you can substitute. +Please keep in mind that this exercise is intended to be achievable in a couple of hours. We expect a production ready submission that demonstrates not only the code you write but quality controls such as exception handling, +logging, unit tests, etc. Assume that this api will be part of a larger system. If there are larger considerations, that would have affected decisions of what is in/out of scope, please make note of your assumptions. +The majority of the tech stack in use at Nuix Discover is SQLServer, C#.NET, ExtJs, Vue.js. We prefer the use of that stack, but we want you to showcase your abilities. If you don't know those specific technologies, +you can substitute. ## Problem statement -The company you are working for is building an investment trading platform for stock, bond and mutual funds. The platform will have various functionality to buy, sell, deposit, withdrawal, and report on investments. For this part of the product, they need you to create a web api to return data for investment performance. The type of api to create is your decision, but it must be hosted on a webserver and accessed via web request. +The company you are working for is building an investment trading platform for stock, bond and mutual funds. The platform will have various functionality to buy, sell, deposit, withdrawal, and report on investments. +For this part of the product, they need you to create a web api to return data for investment performance. The type of api to create is your decision, but it must be hosted on a webserver and accessed via web request. ## Problem details/user stories: - ### Get a list of current investments for the user diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/ControllerTests/UserInvestmentTests.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/ControllerTests/UserInvestmentTests.cs new file mode 100644 index 00000000..9bfd3447 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/ControllerTests/UserInvestmentTests.cs @@ -0,0 +1,121 @@ +using Moq; +using AutoMapper; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.AspNetCore.Mvc; +using AutoFixture; +using InvestmentPerformanceWebApi.Api.Controllers; +using InvestmentPerformanceWebApi.Services.Interfaces; +using InvestmentPerformanceWebApi.Api.ViewModels.Investments; + +namespace InvestmentPerformanceApi.Tests.ControllerTests +{ + public class UserInvestmentsControllerTests + { + private readonly Mock _mockInvestmentService; + private readonly IMapper _mapper; + private readonly Mock> _mockLogger; + private readonly UserInvestmentsController _controller; + + public UserInvestmentsControllerTests() + { + _mockInvestmentService = new Mock(); + _mockLogger = new Mock>(); + + var configExpression = new MapperConfigurationExpression(); + configExpression.CreateMap(); + configExpression.CreateMap(); + + var mapperConfig = new MapperConfiguration(configExpression, NullLoggerFactory.Instance); + _mapper = mapperConfig.CreateMapper(); + + _controller = new UserInvestmentsController( + _mockLogger.Object, + _mockInvestmentService.Object, + _mapper + ); + } + + [Fact] + public async Task GetInvestmentsForUser_ReturnsOk_WithMappedDto() + { + // Arrange + int userId = 1; + var fixture = new Fixture(); + var investments = fixture.CreateMany(3); + + + // Use Task.FromResult to avoid Moq ReturnsAsync issues + _mockInvestmentService + .Setup(s => s.GetInvestmentsForUserAsync(userId)) + .Returns(Task.FromResult(investments)); + // Act + var result = await _controller.GetInvestmentsForUser(userId); + + // Assert + var okResult = Assert.IsType(result.Result); + var returnedDtos = Assert.IsAssignableFrom>(okResult.Value); + Assert.Equal(3, ((List)returnedDtos).Count); + } + [Fact] + public async Task GetSingleInvestmentForUser_ReturnsOk_WithMappedDto() + { + // Arrange + int userId = 1; + + var fixture = new Fixture(); + + var investment = fixture.Create(); + + _mockInvestmentService + .Setup(s => s.GetInvestmentForUserAsync(userId, investment.Id)) + .Returns(Task.FromResult(investment)); + + // Act + var result = await _controller.GetInvestmentForUser(userId, investment.Id); + + // Assert + var okResult = Assert.IsType(result.Result); + + var dto = Assert.IsType(okResult.Value); + + Assert.Equal(investment.Id, dto.Id); + Assert.Equal(investment.Name, dto.Name); + } + + + [Fact] + public async Task GetInvestmentsForUser_ReturnsNotFound_WhenInvestmentDoesNotExist() + { + // Arrange + + _mockInvestmentService + .Setup(s => s.GetInvestmentsForUserAsync(It.IsAny())) + .ThrowsAsync(new KeyNotFoundException("Investment not found")); + + // Act + var result = await _controller.GetInvestmentsForUser(1); + + // Assert + var notFoundResult = Assert.IsType(result.Result); + + Assert.Contains("Investment not found", notFoundResult.Value.ToString()); + } + + [Fact] + public async Task GetSingleInvestmentsForUser_ReturnsNotFound_WhenInvestmentDoesNotExist() + { + // Arrange + _mockInvestmentService + .Setup(s => s.GetInvestmentForUserAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new KeyNotFoundException("Investment not found")); + + // Act + var result = await _controller.GetInvestmentForUser(1, 2); + + // Assert + var notFoundResult = Assert.IsType(result.Result); + Assert.Contains("Investment not found", notFoundResult.Value.ToString()); + } + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/DomainTests/InvestmentTests.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/DomainTests/InvestmentTests.cs new file mode 100644 index 00000000..8cf0a5a2 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/DomainTests/InvestmentTests.cs @@ -0,0 +1,111 @@ +using System; +using InvestmentPerformanceWebApi.Domain.Models; +using Xunit; + +namespace InvestmentPerformanceWebApi.Tests.Domain +{ + public class InvestmentTests + { + [Fact] + public void Constructor_WithNegativeShares_ThrowsArgumentException() + { + // Arrange + string name = "Test Investment"; + int id = 1; + int shares = -5; + decimal costBasis = 100m; + decimal currentPrice = 120m; + DateTime purchasedDate = DateTime.UtcNow; + + // Act & Assert + Assert.Throws(() => + new Investment(name, id, shares, costBasis, currentPrice, purchasedDate)); + } + + [Fact] + public void CurrentValue_ComputesCorrectly() + { + // Arrange + var investment = new Investment( + "Test", + 1, + 10, + 50m, + 60m, + DateTime.UtcNow.AddDays(-10)); + + // Act + var value = investment.CurrentValue; + + // Assert + Assert.Equal(10 * 60m, value); + } + + [Theory] + [InlineData(-10, "Short Term")] + [InlineData(-400, "Long Term")] + [InlineData(365, "Short Term")] + [InlineData(1, "Short Term")] + //[InlineData(100000000000, "Long Term")] //chokes on this, would need to adjust data rules for how long someone can hold an investment for + public void Term_ReturnsCorrectValue(int daysOffset, string expectedTerm) + { + // Arrange + var purchasedDate = DateTime.UtcNow.AddDays(daysOffset); + var investment = new Investment( + "Test", + 1, + 10, + 50m, + 60m, + purchasedDate); + + // Act + var term = investment.Term; + + // Assert + Assert.Equal(expectedTerm, term); + } + + [Fact] + public void TotalGainOrLoss_ComputesCorrectly() + { + // Arrange + var investment = new Investment( + "Test", + 1, + 10, + 50m, + 60m, + DateTime.UtcNow.AddDays(-10)); + + // Act + var totalGain = investment.TotalGainOrLoss; + + // Assert + Assert.Equal((10 * 60m) - (10 * 50m), totalGain); + } + + [Fact] + public void Constructor_InitializesPropertiesCorrectly() + { + // Arrange + string name = "Test Investment"; + int id = 1; + int shares = 10; + decimal costBasis = 50m; + decimal currentPrice = 60m; + DateTime purchasedDate = DateTime.UtcNow; + + // Act + var investment = new Investment(name, id, shares, costBasis, currentPrice, purchasedDate); + + // Assert + Assert.Equal(name, investment.Name); + Assert.Equal(id, investment.Id); + Assert.Equal(shares, investment.Shares); + Assert.Equal(costBasis, investment.CostBasis); + Assert.Equal(currentPrice, investment.CurrentPrice); + Assert.Equal(purchasedDate, investment.PurchasedUtc); + } + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/InvestmentPerformanceApi.Tests.csproj b/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/InvestmentPerformanceApi.Tests.csproj new file mode 100644 index 00000000..ae7d9e5a --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/InvestmentPerformanceApi.Tests.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/ServiceTests/InvestmentServiceTests.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/ServiceTests/InvestmentServiceTests.cs new file mode 100644 index 00000000..e4cc4386 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceApi.Tests/ServiceTests/InvestmentServiceTests.cs @@ -0,0 +1,100 @@ +using InvestmentPerformanceWebApi.DbInfrastructure; +using InvestmentPerformanceWebApi.DbInfrastructure.Models; +using InvestmentPerformanceWebApi.Domain.Services; +using Microsoft.EntityFrameworkCore; + + +public class InvestmentServiceTests +{ + private AppDbContext GetInMemoryDbContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) // unique DB for each test + .Options; + + return new AppDbContext(options); + } + + [Fact] + public async Task GetInvestmentsForUserAsync_UserExists_ReturnsInvestments() + { + // Arrange + var context = GetInMemoryDbContext(); + + var user = new User { Id = 1, FirstName = "Alice", LastName = "Slim", UserName = "salice" }; + context.Users.Add(user); + context.Investments.Add(new Investment + { + Id = 10, + UserId = user.Id, + Name = "Stock A", + Shares = 5, + CostBasis = 100, + CurrentPrice = 120, + PurchasedAtUtc = DateTime.UtcNow + }); + await context.SaveChangesAsync(); + + var service = new InvestmentService(context); + + // Act + var investments = await service.GetInvestmentsForUserAsync(user.Id); + + // Assert + Assert.Single(investments); + Assert.Equal("Stock A", investments.First().Name); + } + + [Fact] + public async Task GetInvestmentsForUserAsync_UserDoesNotExist_ThrowsKeyNotFoundException() + { + // Arrange + var context = GetInMemoryDbContext(); + var service = new InvestmentService(context); + + // Act & Assert + await Assert.ThrowsAsync(() => service.GetInvestmentsForUserAsync(999)); + } + + [Fact] + public async Task GetInvestmentForUserAsync_InvestmentExists_ReturnsInvestment() + { + // Arrange + var context = GetInMemoryDbContext(); + + var user = new User { Id = 1, FirstName = "Alice", LastName = "Slim", UserName = "salice" }; + context.Users.Add(user); + var investment = new Investment + { + Id = 10, + UserId = user.Id, + Name = "Stock A", + Shares = 5, + CostBasis = 100, + CurrentPrice = 120, + PurchasedAtUtc = DateTime.UtcNow + }; + context.Investments.Add(investment); + await context.SaveChangesAsync(); + + var service = new InvestmentService(context); + + // Act + var result = await service.GetInvestmentForUserAsync(user.Id, investment.Id); + + // Assert + Assert.NotNull(result); + Assert.Equal(investment.Id, result.Id); + } + + [Fact] + public async Task GetInvestmentForUserAsync_InvestmentDoesNotExist_ThrowsKeyNotFoundException() + { + // Arrange + var context = GetInMemoryDbContext(); + var service = new InvestmentService(context); + + // Act & Assert + await Assert.ThrowsAsync(() => service.GetInvestmentForUserAsync(1, 999)); + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.sln b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.sln new file mode 100644 index 00000000..f599fd8f --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36811.4 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceWebApi", "InvestmentPerformanceWebApi\InvestmentPerformanceWebApi.csproj", "{42DE2698-83AE-4E5E-81BF-C919DDD7E84B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InvestmentPerformanceApi.Tests", "InvestmentPerformanceApi.Tests\InvestmentPerformanceApi.Tests.csproj", "{F237000A-0889-4FAD-AA52-734DA54D833D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {42DE2698-83AE-4E5E-81BF-C919DDD7E84B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {42DE2698-83AE-4E5E-81BF-C919DDD7E84B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {42DE2698-83AE-4E5E-81BF-C919DDD7E84B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {42DE2698-83AE-4E5E-81BF-C919DDD7E84B}.Release|Any CPU.Build.0 = Release|Any CPU + {F237000A-0889-4FAD-AA52-734DA54D833D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F237000A-0889-4FAD-AA52-734DA54D833D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F237000A-0889-4FAD-AA52-734DA54D833D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F237000A-0889-4FAD-AA52-734DA54D833D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {56360198-CE0B-4098-96BC-C825BC9B957A} + EndGlobalSection +EndGlobal diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Controllers/UserInvestmentsController.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Controllers/UserInvestmentsController.cs new file mode 100644 index 00000000..1d805e88 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Controllers/UserInvestmentsController.cs @@ -0,0 +1,84 @@ +using InvestmentPerformanceWebApi.DbInfrastructure.Models; +using InvestmentPerformanceWebApi.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; +using AutoMapper; +using InvestmentPerformanceWebApi.Api.ViewModels.Investments; + +namespace InvestmentPerformanceWebApi.Api.Controllers +{ + [ApiController] + [Route("users/{userid}/investments")] + public class UserInvestmentsController : ControllerBase + { + private readonly ILogger _logger; + private readonly IMapper _mapper; + public IInvestmentService _investmentService; + + public UserInvestmentsController(ILogger logger, IInvestmentService investmentService, IMapper mapper) + { + _logger = logger; + _investmentService = investmentService; + _mapper = mapper; + } + + [HttpGet()] + public async Task>> GetInvestmentsForUser( + int userId) + { + + try + { + var domainInvestments = await _investmentService.GetInvestmentsForUserAsync(userId); + var summaryDtos = _mapper.Map>(domainInvestments); + + return Ok(summaryDtos); + } + catch (KeyNotFoundException ex) + { + _logger.LogWarning(ex, $"Resource not found for userId {userId}"); + return NotFound(new { message = ex.Message }); + + } + catch (Exception ex) + { + return HandleException(ex, userId); + } + + } + + [HttpGet("{investmentId}")] + public async Task> GetInvestmentForUser(int userId, int investmentId) + { + try + { + var domainInvestment = await _investmentService.GetInvestmentForUserAsync(userId, investmentId); + var detailedInvestmentDto = _mapper.Map(domainInvestment); + + return Ok(detailedInvestmentDto); + } + catch (KeyNotFoundException ex) + { + _logger.LogWarning(ex, $"Resource not found for userId: {userId} and investmentId: {investmentId}"); + return NotFound(new {message = ex.Message}); + + } + catch (Exception ex) + { + return HandleException(ex, userId); + + } + } + + private ActionResult HandleException(Exception ex, int userId) + { + var traceId = HttpContext.TraceIdentifier; + var controllerName = ControllerContext.ActionDescriptor.ControllerName; + var actionName = ControllerContext.ActionDescriptor.ActionName; + var endpointPath = HttpContext.Request.Path; + + _logger.LogError(ex, $"ERROR: {controllerName} - {actionName}. Error reference TraceId: {traceId}"); + return StatusCode(500, $"Error accessing {endpointPath} for UserId: {userId}. Reference TraceId: {traceId}"); + + } + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/InvestmentPerformanceWebApi.http b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/InvestmentPerformanceWebApi.http new file mode 100644 index 00000000..be8b5067 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/InvestmentPerformanceWebApi.http @@ -0,0 +1,6 @@ +@InvestmentPerformanceWebApi_HostAddress = http://localhost:5019 + +GET {{InvestmentPerformanceWebApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Mappings/InvestmentMappingProfile.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Mappings/InvestmentMappingProfile.cs new file mode 100644 index 00000000..55ed6999 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Mappings/InvestmentMappingProfile.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using InvestmentPerformanceWebApi.Api.ViewModels.Investments; +using InvestmentPerformanceWebApi.Domain.Models; + +namespace InvestmentPerformanceWebApi.Api.Mappings +{ + public class InvestmentMappingProfile : Profile + { + public InvestmentMappingProfile() + { + CreateMap(); + CreateMap(); + } + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Program.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Program.cs new file mode 100644 index 00000000..b8348589 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/Program.cs @@ -0,0 +1,49 @@ +using InvestmentPerformanceWebApi.DbInfrastructure; +using InvestmentPerformanceWebApi.Services.Interfaces; +using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using System.Text.Json.Serialization; +using InvestmentPerformanceWebApi.Domain.Services; +using InvestmentPerformanceWebApi.Api.Mappings; + +var builder = WebApplication.CreateBuilder(args); + + +builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add( + new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); + }); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddDbContext(options => options.UseSqlite("Data Source=app.db")); +builder.Services.AddScoped(); + +builder.Services.AddAutoMapper(cfg => +{ + cfg.AddProfile(); +}); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); //this would need adjusted if we were doing migrations, i dont have a database existing yet so for simplicity im just ensuring created here for it to create and seed the database +} + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/ViewModels/Investments/InvestmentDetailView.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/ViewModels/Investments/InvestmentDetailView.cs new file mode 100644 index 00000000..706c581e --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/ViewModels/Investments/InvestmentDetailView.cs @@ -0,0 +1,18 @@ +using System.Runtime.Serialization; +using System.Text.Json.Serialization; + +namespace InvestmentPerformanceWebApi.Api.ViewModels.Investments +{ + public class InvestmentDetailView + { + public string Name { get; set; } = string.Empty; + public int Id { get; set; } + public int Shares { get; set; } + public decimal CostBasis { get; set; } + public decimal CurrentPrice { get; set; } + public decimal CurrentValue { get; set; } + public decimal TotalGainOrLoss { get; set; } + public string Term { get; set; } = string.Empty; + } + +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/ViewModels/Investments/InvestmentSummaryView.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/ViewModels/Investments/InvestmentSummaryView.cs new file mode 100644 index 00000000..6af4c26b --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/ViewModels/Investments/InvestmentSummaryView.cs @@ -0,0 +1,8 @@ +namespace InvestmentPerformanceWebApi.Api.ViewModels.Investments +{ + public class InvestmentSummaryView + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/appsettings.Development.json b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/appsettings.json b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Api/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/AppDbContext.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/AppDbContext.cs new file mode 100644 index 00000000..8f8daecf --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/AppDbContext.cs @@ -0,0 +1,91 @@ +using InvestmentPerformanceWebApi.DbInfrastructure.Models; +using Microsoft.EntityFrameworkCore; + +namespace InvestmentPerformanceWebApi.DbInfrastructure +{ + public class AppDbContext : DbContext + { + public AppDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Users => Set(); + public DbSet Investments => Set(); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + + modelBuilder.Entity() + .HasKey(t => t.Id); + + + modelBuilder.Entity(investment => + { + investment.HasKey(t => t.Id); + + investment.Property(i => i.Id) + .ValueGeneratedNever(); + + investment.HasOne(t => t.User) + .WithMany(u => u.Investments) + .HasForeignKey(u => u.UserId) + .IsRequired(); + + investment.HasIndex(i => i.UserId); + + }); + + var user1 = Guid.NewGuid(); + + modelBuilder.Entity().HasData( + new User + { + Id = 1, + UserName = "becho", + FirstName = "Echo", + LastName = "Bazaz", + CreatedUtc = DateTime.UtcNow, + LastUpdatedUtc = DateTime.UtcNow + } + ); + + modelBuilder.Entity().HasData( + new Investment + { + Id = 1, + UserId = 1, + Name = "Stock A", + Shares = 20, + CostBasis = 11.75m, + CurrentPrice = 80.90m, + PurchasedAtUtc = DateTime.UtcNow.AddMonths(-6), + CreatedAtUtc = DateTime.UtcNow + }, + new Investment + { + Id = 2, + UserId = 1, + Name = "Stock B", + Shares = 8, + CostBasis = 50.00m, + CurrentPrice = 45.69m, + PurchasedAtUtc = DateTime.UtcNow.AddDays(-3), + CreatedAtUtc = DateTime.UtcNow + }, + new Investment + { + Id = 3, + UserId = 1, + Name = "Stock C", + Shares = 2, + CostBasis = 50.00m, + CurrentPrice = 250.00m, + PurchasedAtUtc = DateTime.UtcNow.AddYears(-10), + CreatedAtUtc = DateTime.UtcNow + } + ); + } + + + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/Models/Investment.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/Models/Investment.cs new file mode 100644 index 00000000..1b76fd03 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/Models/Investment.cs @@ -0,0 +1,17 @@ + +namespace InvestmentPerformanceWebApi.DbInfrastructure.Models +{ + public class Investment + { + public int Id { get; set; } + public int UserId { get; set; } + public User User { get; set; } = null!; + public string Name { get; set; } = null!; + public int Shares { get; set; } + public decimal CostBasis { get; set; } //price of 1 share of stock at time it was purchased + public decimal CurrentPrice { get; set; } // current price of 1 share of stock + public DateTime PurchasedAtUtc { get; set; } // can be used to calculate the Term + public DateTime CreatedAtUtc { get; set; } + + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/Models/User.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/Models/User.cs new file mode 100644 index 00000000..d3655857 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/DbInfrastructure/Models/User.cs @@ -0,0 +1,15 @@ + +namespace InvestmentPerformanceWebApi.DbInfrastructure.Models +{ + public class User + { + public int Id { get; set; } + public string UserName { get; set; } = null!; + public string FirstName { get; set; } = null!; + public string LastName { get; set; } = null!; + public DateTime CreatedUtc { get; set; } + public DateTime LastUpdatedUtc { get; set; } + public ICollection Investments { get; set; } = null!; + + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Interfaces/IInvestmentService.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Interfaces/IInvestmentService.cs new file mode 100644 index 00000000..f6732a33 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Interfaces/IInvestmentService.cs @@ -0,0 +1,10 @@ +using InvestmentPerformanceWebApi.Domain.Models; + +namespace InvestmentPerformanceWebApi.Services.Interfaces +{ + public interface IInvestmentService + { + Task> GetInvestmentsForUserAsync(int userId); + Task GetInvestmentForUserAsync(int userId, int investmentId); + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Models/Investment.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Models/Investment.cs new file mode 100644 index 00000000..f4f0ffba --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Models/Investment.cs @@ -0,0 +1,47 @@ + + +namespace InvestmentPerformanceWebApi.Domain.Models +{ + public class Investment + { + public string Name { get; private set; } + public int Id { get; private set; } + public int Shares { get; private set; } + public decimal CostBasis { get; private set; } + public decimal CurrentPrice { get; private set; } + public DateTime PurchasedUtc { get; private set; } + + public decimal CurrentValue => Shares * CurrentPrice; + + public string Term => GetTerm(DateTime.UtcNow); + public decimal TotalGainOrLoss => Shares * CurrentPrice - Shares * CostBasis; + + + private string GetTerm(DateTime asOf) + { + return (asOf - PurchasedUtc).TotalDays >= 365 + ? "Long Term" + : "Short Term"; + } + + public Investment( + string name, + int id, + int shares, + decimal costBasis, + decimal currentPrice, + DateTime purchasedDateUtc) + { + if (shares < 0) + { + throw new ArgumentException("Shares must be greater than 0"); + } + Name = name; + Id = id; + Shares = shares; + CostBasis = costBasis; + CurrentPrice = currentPrice; + PurchasedUtc = purchasedDateUtc; + } + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Services/InvestmentService.cs b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Services/InvestmentService.cs new file mode 100644 index 00000000..847762b4 --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Domain/Services/InvestmentService.cs @@ -0,0 +1,50 @@ +using InvestmentPerformanceWebApi.DbInfrastructure; +using InvestmentPerformanceWebApi.Services.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace InvestmentPerformanceWebApi.Domain.Services +{ + public class InvestmentService : IInvestmentService + { + private readonly AppDbContext _context; + + public InvestmentService(AppDbContext context) + { + _context = context; + } + + public async Task> GetInvestmentsForUserAsync(int userId) + { + var userExists = await _context.Users.AnyAsync(u => u.Id == userId); + if(!userExists) + { + throw new KeyNotFoundException($"User with Id {userId} not found"); + } + + var investments = await _context.Investments + .Where(i => i.UserId == userId) + .Select(x => new Models.Investment(x.Name, x.Id, x.Shares, x.CostBasis, x.CurrentPrice, x.PurchasedAtUtc)) + .ToListAsync(); + + return investments; + + + } + + public async Task GetInvestmentForUserAsync(int userId, int investmentId) + { + + var investment = await _context.Investments + .Where(i => i.UserId == userId && i.Id == investmentId) + .Select(x => new Models.Investment(x.Name, x.Id, x.Shares, x.CostBasis, x.CurrentPrice, x.PurchasedAtUtc)) + .FirstOrDefaultAsync(); + + if (investment is null) + { + throw new KeyNotFoundException($"Either Investment {investmentId} or user {userId} invalid"); + } + + return investment; + } + } +} diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.csproj b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.csproj new file mode 100644 index 00000000..4b6b9b2a --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.csproj.user b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.csproj.user new file mode 100644 index 00000000..9ff5820a --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Properties/launchSettings.json b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Properties/launchSettings.json new file mode 100644 index 00000000..d6581c4a --- /dev/null +++ b/InvestmentPerformanceWebApi/InvestmentPerformanceWebApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14211", + "sslPort": 44348 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5019", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7130;http://localhost:5019", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/InvestmentPerformanceWebApi/README.md b/InvestmentPerformanceWebApi/README.md new file mode 100644 index 00000000..ad209b51 --- /dev/null +++ b/InvestmentPerformanceWebApi/README.md @@ -0,0 +1,36 @@ +# INITIAL NOTES +- this is eventually going to be a bigger system, which might or might not have a database already existing, with user and/or investment table +- will eventually have buy, sell, deposit, withrawal, report etc. on investments +- two required functionalities, + 1. get investments for user (return investmentid and name) + - IMPORTANT: doesnt note if its the name of the INVESTMENT OR name of the USER + 2. Get investment by ID (should return shares, cost per share, current value, current price, term, and total gain/loss) + + +# ASSUMPTIONS: +- For the sake of the exercise, i am going to assume that the database and tables do not exist since we were not given any model/entity requirements of what a user and investment are +- We are ONLY returning data, there are no posts/inserts into the database, so entity validations arent necessarily required at this stage +- Assumed this is an online portal that you can log into, we only want the logged in user to access their own investments + - I would probably adjust the endpoints, if there was an admin or view all page for needs not specific to one users investments (i.e. /investments standalone endpoint) +- Assumed that investments dont exist without a user +- Assumed that an investment can only belong to one user +- I am going to assume that for the current price of the investment, there is another process that updates the prices and current value on the investments table dynamically + - for stocks, the prices change constantly - so these values wouldnt necesarily be stored in the actual investment entity itself maybe another reference table or pulled from a live API + - Id possibly consider storing the derived values on the Investmain domain object in the database, potentially in the same process that updates the prices +- assumed that the stock system deals in a single currency - USD $$ +- assumed that the "name" required to return was the stock name, not the users name + + +# RUNNING APPLICATION: +- This Application uses Sqlite, with auto seeding registered in the program.cs. no database migrations need to be run for this to spin up and database to get created +- endpoints are as follows: users/{userId}/investments/ and users/{userId}/investments/{investmentId} +- check the startup window for the port that the API is listening on locally +- Seed data can be found in AppDbContext.cs + - Included user 1 and investments 1, 2 and 3. + +# NOTES +- debated on wheter to use 3 model layer (view, domain model with logic, and db entity) + - but wanted to keep the view separate from the domain, since we can remove/add fields to the view model as needed + - if the calculated fields were already stored in database, this would more than likely change + + From 52b633bd50c807aaa1c037a654f21cf0ce732080 Mon Sep 17 00:00:00 2001 From: Erin Bazaz <50595616+bazazer@users.noreply.github.com> Date: Mon, 22 Dec 2025 08:42:11 -0500 Subject: [PATCH 3/3] reformatted readme --- InvestmentPerformanceWebApi/README.md | 87 ++++++++++++++++----------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/InvestmentPerformanceWebApi/README.md b/InvestmentPerformanceWebApi/README.md index ad209b51..ab6d162f 100644 --- a/InvestmentPerformanceWebApi/README.md +++ b/InvestmentPerformanceWebApi/README.md @@ -1,36 +1,55 @@ -# INITIAL NOTES -- this is eventually going to be a bigger system, which might or might not have a database already existing, with user and/or investment table -- will eventually have buy, sell, deposit, withrawal, report etc. on investments -- two required functionalities, - 1. get investments for user (return investmentid and name) - - IMPORTANT: doesnt note if its the name of the INVESTMENT OR name of the USER - 2. Get investment by ID (should return shares, cost per share, current value, current price, term, and total gain/loss) - - -# ASSUMPTIONS: -- For the sake of the exercise, i am going to assume that the database and tables do not exist since we were not given any model/entity requirements of what a user and investment are -- We are ONLY returning data, there are no posts/inserts into the database, so entity validations arent necessarily required at this stage -- Assumed this is an online portal that you can log into, we only want the logged in user to access their own investments - - I would probably adjust the endpoints, if there was an admin or view all page for needs not specific to one users investments (i.e. /investments standalone endpoint) -- Assumed that investments dont exist without a user -- Assumed that an investment can only belong to one user -- I am going to assume that for the current price of the investment, there is another process that updates the prices and current value on the investments table dynamically - - for stocks, the prices change constantly - so these values wouldnt necesarily be stored in the actual investment entity itself maybe another reference table or pulled from a live API - - Id possibly consider storing the derived values on the Investmain domain object in the database, potentially in the same process that updates the prices -- assumed that the stock system deals in a single currency - USD $$ -- assumed that the "name" required to return was the stock name, not the users name - - -# RUNNING APPLICATION: -- This Application uses Sqlite, with auto seeding registered in the program.cs. no database migrations need to be run for this to spin up and database to get created -- endpoints are as follows: users/{userId}/investments/ and users/{userId}/investments/{investmentId} -- check the startup window for the port that the API is listening on locally -- Seed data can be found in AppDbContext.cs - - Included user 1 and investments 1, 2 and 3. - -# NOTES -- debated on wheter to use 3 model layer (view, domain model with logic, and db entity) - - but wanted to keep the view separate from the domain, since we can remove/add fields to the view model as needed - - if the calculated fields were already stored in database, this would more than likely change +# Investment System - README +## Overview +**Current Required Functionalities:** +1. **Get investments for a user** + - Returns investment ID and investment name. + - **Note:** This returns the **investment name**, not the user's name. +2. **Get investment by ID** + - Returns details including: + - Shares + - Cost per share + - Current value + - Current price + - Term + - Total gain/loss + +--- + +## Assumptions +- The database and tables do **not exist** yet; no predefined entity models were provided. +- This system is **read-only** at this stage (no inserts/updates). +- The system is assumed to be an **online portal**, and users can only access **their own investments**. +- Investments cannot exist without a user, and an investment belongs to **only one user**. +- Current price and value of investments are updated dynamically (e.g., via a separate process or live API). +- All investments are assumed to be in a **single currency (USD)**. +- The "name" returned by the API refers to the **investment name**, not the user name. + +**Additional Notes on Design:** +- Considered a 3-layer architecture (View, Domain Model with logic, DB Entity). + - Views(DTOs) are kept separate from the domain model for flexibility. + - If calculated fields were stored in the database, design decisions may change. + +--- + +## Running the Application +- Uses **SQLite** as the database. +- Auto-seeding of data is configured in `Program.cs`. No migrations are needed. +- **Endpoints:** + - `GET /users/{userId}/investments/` → returns all investments for a user + - `GET /users/{userId}/investments/{investmentId}` → returns investment details by ID +- Check the startup window to see which **port** the API is running on locally. +- **Seed Data:** + - Defined in `AppDbContext.cs` + - Includes User 1 and Investments 1, 2, and 3 + +--- + +## Future Considerations +- Support for multiple currencies +- Integration with live stock price APIs +- Extended features for admin users or multi-user investment views +- Buy/sell, deposit, withdrawal, and reporting functionalities +- validations when data inserted into database, determine validation rules +- Is there need to calculate gain/loss and total value outside of investment domain model?