Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
484 changes: 484 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

112 changes: 112 additions & 0 deletions InvestmentPerformanceApi.Tests/InvestmentControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
using Xunit;
using Moq;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Linq;
using System;

using InvestmentPerformanceApi.Controllers;
using InvestmentPerformanceApi.Models;
using InvestmentPerformanceApi.Repos;

public class InvestmentControllerTests
{
private readonly Mock<IInvestmentRepository> _mockRepo;
private readonly Mock<ILogger<InvestmentController>> _mockLogger;
private readonly InvestmentController _controller;

public InvestmentControllerTests()
{
_mockRepo = new Mock<IInvestmentRepository>();
_mockLogger = new Mock<ILogger<InvestmentController>>();
_controller = new InvestmentController(_mockRepo.Object, _mockLogger.Object);
}

[Fact]
public void GetInvestments_ReturnsOk_WhenDataExists()
{
// Arrange
int userId = 1;
_mockRepo.Setup(r => r.GetByUserId(userId)).Returns(new List<Investment>
{
new Investment { Id = 1, Name = "Apple", UserId = userId },
new Investment { Id = 2, Name = "Google", UserId = userId }
});

// Act
var result = _controller.GetInvestments(userId);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var investments = Assert.IsAssignableFrom<IEnumerable<object>>(okResult.Value);
Assert.Equal(2, investments.Count());
}

[Fact]
public void GetInvestments_Returns500_OnException()
{
// Arrange
int userId = 1;
_mockRepo.Setup(r => r.GetByUserId(userId)).Throws(new Exception("DB failure"));

// Act
var result = _controller.GetInvestments(userId);

// Assert
var errorResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(500, errorResult.StatusCode);
}

[Fact]
public void GetInvestmentDetails_ReturnsOk_WhenFound()
{
// Arrange
int userId = 1, investmentId = 1;
_mockRepo.Setup(r => r.GetById(userId, investmentId)).Returns(new Investment
{
Id = investmentId,
UserId = userId,
Shares = 10,
CostBasisPerShare = 100,
CurrentPrice = 150,
PurchaseDate = DateTime.UtcNow.AddMonths(-6)
});

// Act
var result = _controller.GetInvestmentDetails(userId, investmentId);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
Assert.NotNull(okResult.Value);
}

[Fact]
public void GetInvestmentDetails_ReturnsNotFound_WhenNull()
{
// Arrange
int userId = 1, investmentId = 99;
_mockRepo.Setup(r => r.GetById(userId, investmentId)).Returns((Investment)null);

// Act
var result = _controller.GetInvestmentDetails(userId, investmentId);

// Assert
Assert.IsType<NotFoundResult>(result);
}

[Fact]
public void GetInvestmentDetails_Returns500_OnException()
{
// Arrange
int userId = 1, investmentId = 1;
_mockRepo.Setup(r => r.GetById(userId, investmentId)).Throws(new Exception("Unexpected failure"));

// Act
var result = _controller.GetInvestmentDetails(userId, investmentId);

// Assert
var errorResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(500, errorResult.StatusCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\InvestmentPerformanceApi\InvestmentPerformanceApi.csproj" />
</ItemGroup>

</Project>
69 changes: 69 additions & 0 deletions InvestmentPerformanceApi/Controllers/InvestmentController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using Microsoft.AspNetCore.Mvc;
using InvestmentPerformanceApi.Repos;

namespace InvestmentPerformanceApi.Controllers;

[ApiController]
[Route("api/users/{userId}/investments")]
public class InvestmentController : ControllerBase
{
private readonly IInvestmentRepository _repository;
private readonly ILogger<InvestmentController> _logger;

public InvestmentController(IInvestmentRepository repository, ILogger<InvestmentController> logger)
{
_repository = repository;
_logger = logger;
}

[HttpGet]
public IActionResult GetInvestments(int userId)
{
try
{
_logger.LogInformation("Fetching investments for user {UserId}", userId);

var investments = _repository.GetByUserId(userId)
.Select(i => new { i.Id, i.Name });

return Ok(investments);
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching investments for user {UserId}", userId);
return StatusCode(500, new { message = "Something went wrong while processing your request." });
}
}

[HttpGet("{investmentId}")]
public IActionResult GetInvestmentDetails(int userId, int investmentId)
{
try
{
_logger.LogInformation("Fetching details for investment {InvestmentId} for user {UserId}", investmentId, userId);

var investment = _repository.GetById(userId, investmentId);

if (investment == null)
{
_logger.LogWarning("Investment {InvestmentId} not found for user {UserId}", investmentId, userId);
return NotFound();
}

return Ok(new
{
investment.Shares,
investment.CostBasisPerShare,
investment.CurrentPrice,
investment.CurrentValue,
investment.Term,
TotalGainOrLoss = investment.TotalGainOrLoss
});
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while fetching investment {InvestmentId} for user {UserId}", investmentId, userId);
return StatusCode(500, new { message = "Something went wrong while processing your request." });
}
}
}
14 changes: 14 additions & 0 deletions InvestmentPerformanceApi/InvestmentPerformanceApi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
</ItemGroup>

</Project>
24 changes: 24 additions & 0 deletions InvestmentPerformanceApi/Models/Investment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace InvestmentPerformanceApi.Models;

public class Investment
{
public int Id { get; set; }

public string Name { get; set; } = string.Empty;

public int UserId { get; set; }

public int Shares { get; set; }

public decimal CostBasisPerShare { get; set; }

public decimal CurrentPrice { get; set; }

public DateTime PurchaseDate { get; set; }

public decimal CurrentValue => Shares * CurrentPrice;

public string Term => (DateTime.UtcNow - PurchaseDate).TotalDays > 365 ? "Long Term" : "Short Term";

public decimal TotalGainOrLoss => CurrentValue - (Shares * CostBasisPerShare);
}
17 changes: 17 additions & 0 deletions InvestmentPerformanceApi/Models/User.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace InvestmentPerformanceApi.Models;

public class User
{
public int Id { get; set; }

public string FirstName { get; set; }

public string LastName { get; set; }

public string Email { get; set; }

public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

public ICollection<Investment> Investments { get; set; }
}

30 changes: 30 additions & 0 deletions InvestmentPerformanceApi/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using InvestmentPerformanceApi.Repos;
using Microsoft.OpenApi.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Investment Performance API", Version = "v1" });
});

builder.Services.AddSingleton<InvestmentRepository>();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Investment Performance API v1");
c.RoutePrefix = string.Empty;
});

//app.UseHttpsRedirection();

app.MapControllers();

app.Run();

15 changes: 15 additions & 0 deletions InvestmentPerformanceApi/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"NuixInvestmentApi": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7116;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

10 changes: 10 additions & 0 deletions InvestmentPerformanceApi/Repos/IInvestmentRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using InvestmentPerformanceApi.Models;

namespace InvestmentPerformanceApi.Repos;

public interface IInvestmentRepository
{
IEnumerable<Investment> GetByUserId(int userId);

Investment? GetById(int userId, int investmentId);
}
23 changes: 23 additions & 0 deletions InvestmentPerformanceApi/Repos/InvestmentRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using InvestmentPerformanceApi.Models;

namespace InvestmentPerformanceApi.Repos;

public class InvestmentRepository : IInvestmentRepository
{
private readonly List<Investment> _investments = new()
{
new Investment { Id = 1, Name = "Apple", UserId = 1, Shares = 10, CostBasisPerShare = 150, CurrentPrice = 170, PurchaseDate = DateTime.UtcNow.AddMonths(-14) },
new Investment { Id = 2, Name = "Google", UserId = 1, Shares = 5, CostBasisPerShare = 1200, CurrentPrice = 1400, PurchaseDate = DateTime.UtcNow.AddMonths(-10) },
new Investment { Id = 3, Name = "Microsoft", UserId = 1, Shares = 2, CostBasisPerShare = 450, CurrentPrice = 435, PurchaseDate = DateTime.UtcNow.AddMonths(-2) },
new Investment { Id = 4, Name = "Amazon", UserId = 1, Shares = 8, CostBasisPerShare = 2010, CurrentPrice = 1950, PurchaseDate = DateTime.UtcNow.AddMonths(-16) },
new Investment { Id = 5, Name = "Oracle", UserId = 1, Shares = 15, CostBasisPerShare = 150, CurrentPrice = 162, PurchaseDate = DateTime.UtcNow.AddMonths(-1) },
new Investment { Id = 6, Name = "Dell", UserId = 1, Shares = 5, CostBasisPerShare = 100, CurrentPrice = 107, PurchaseDate = DateTime.UtcNow.AddMonths(-5) },
new Investment { Id = 7, Name = "Google", UserId = 2, Shares = 100, CostBasisPerShare = 1200, CurrentPrice = 1400, PurchaseDate = DateTime.UtcNow.AddMonths(-6) },
new Investment { Id = 8, Name = "Nvidia", UserId = 2, Shares = 150, CostBasisPerShare = 1300, CurrentPrice = 1330, PurchaseDate = DateTime.UtcNow.AddMonths(-6) },
new Investment { Id = 9, Name = "Netflix", UserId = 1, Shares = 12, CostBasisPerShare = 1050, CurrentPrice = 1130, PurchaseDate = DateTime.UtcNow.AddMonths(-3) },
new Investment { Id = 10, Name = "Google", UserId = 3, Shares = 10000, CostBasisPerShare = 1200, CurrentPrice = 1400, PurchaseDate = DateTime.UtcNow.AddYears(-3) },
};

public IEnumerable<Investment> GetByUserId(int userId) => _investments.Where(i => i.UserId == userId);
public Investment? GetById(int userId, int investmentId) => _investments.FirstOrDefault(i => i.UserId == userId && i.Id == investmentId);
}
8 changes: 8 additions & 0 deletions InvestmentPerformanceApi/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions InvestmentPerformanceApi/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Loading