diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..b2aea9d2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+# Build outputs
+bin/
+obj/
+
+# User-specific files
+*.user
+*.suo
+
+# Visual Studio
+.vs/
+
+# NuGet packages
+packages/
+
+# Environment files
+.env
+appsettings.local.json
+
+# Test results
+TestResults/
+coverage/
+
+# OS files
+.DS_Store
+Thumbs.db
diff --git a/CodingExercise.Tests/CodingExercise.Tests.csproj b/CodingExercise.Tests/CodingExercise.Tests.csproj
new file mode 100644
index 00000000..2696389a
--- /dev/null
+++ b/CodingExercise.Tests/CodingExercise.Tests.csproj
@@ -0,0 +1,28 @@
+ο»Ώ
+
+
+ net9.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CodingExercise.Tests/InvestmentServiceTests.cs b/CodingExercise.Tests/InvestmentServiceTests.cs
new file mode 100644
index 00000000..7a30e641
--- /dev/null
+++ b/CodingExercise.Tests/InvestmentServiceTests.cs
@@ -0,0 +1,169 @@
+using Microsoft.Extensions.Logging;
+using FluentAssertions;
+using Moq;
+using Xunit;
+using CodingExercise.Services;
+using CodingExercise.Models;
+
+namespace CodingExercise.Tests;
+
+public class InvestmentServiceTests
+{
+ private readonly Mock> _mockLogger;
+ private readonly InvestmentService _service;
+
+ public InvestmentServiceTests()
+ {
+ _mockLogger = new Mock>();
+ _service = new InvestmentService(_mockLogger.Object);
+ }
+
+ // Tests that the service returns investment data for a valid existing user.
+ // Should return a non-empty list with valid investment summaries.
+ [Fact]
+ public async Task GetUserInvestments_ExistingUser_ReturnsInvestments()
+ {
+ var userId = "user1";
+
+ var result = await _service.GetUserInvestments(userId);
+
+ result.Should().NotBeEmpty();
+ result.Should().AllSatisfy(investment =>
+ {
+ investment.Id.Should().BePositive();
+ investment.Name.Should().NotBeNullOrEmpty();
+ });
+ }
+
+ // Tests that the service returns an empty list for users that don't exist.
+ // Should return an empty collection rather than throwing an exception.
+ [Fact]
+ public async Task GetUserInvestments_NonExistentUser_ReturnsEmptyList()
+ {
+ var userId = "nonexistentuser";
+
+ var result = await _service.GetUserInvestments(userId);
+
+ result.Should().BeEmpty();
+ }
+
+ // Tests that user ID matching is case-insensitive.
+ // Should return same results for "user1" and "USER1".
+ [Fact]
+ public async Task GetUserInvestments_CaseInsensitive_ReturnsInvestments()
+ {
+ var userId = "USER1"; // Uppercase version
+
+ var result = await _service.GetUserInvestments(userId);
+
+ result.Should().NotBeEmpty();
+ }
+
+ // Tests that the service returns complete investment details for valid investment IDs.
+ [Fact]
+ public async Task GetInvestmentDetails_ExistingInvestment_ReturnsDetails()
+ {
+ var investmentId = 1; // Apple investment
+
+ var result = await _service.GetInvestmentDetails(investmentId);
+
+ result.Should().NotBeNull();
+ result!.Id.Should().Be(investmentId);
+ result.Name.Should().Be("Apple");
+ result.Shares.Should().BePositive();
+ result.CostBasisPerShare.Should().BePositive();
+ result.CurrentPrice.Should().BePositive();
+ result.CurrentValue.Should().BePositive();
+ result.Term.Should().BeOneOf("Short Term", "Long Term");
+ }
+
+ // Tests that the service returns null for investment IDs that don't exist.
+ [Fact]
+ public async Task GetInvestmentDetails_NonExistentInvestment_ReturnsNull()
+ {
+ var investmentId = 999;
+
+ var result = await _service.GetInvestmentDetails(investmentId);
+
+ result.Should().BeNull();
+ }
+
+ // Tests that investments with gains show positive total gain/loss values.
+ [Theory]
+ [InlineData(1)] // Apple - should be profitable
+ [InlineData(3)] // Google - should be profitable
+ [InlineData(5)] // Meta - should be profitable
+ public async Task GetInvestmentDetails_ProfitableInvestments_HasPositiveGains(int investmentId)
+ {
+ var result = await _service.GetInvestmentDetails(investmentId);
+
+ result.Should().NotBeNull();
+ result!.TotalGainLoss.Should().BePositive();
+ result.CurrentValue.Should().BeGreaterThan(result.Shares * result.CostBasisPerShare);
+ }
+
+ // Tests that investments with losses show negative total gain/loss values.
+ [Theory]
+ [InlineData(2)] // Microsoft - should be loss
+ [InlineData(4)] // Tesla - should be loss
+ public async Task GetInvestmentDetails_LossInvestments_HasNegativeGains(int investmentId)
+ {
+ var result = await _service.GetInvestmentDetails(investmentId);
+
+ result.Should().NotBeNull();
+ result!.TotalGainLoss.Should().BeNegative();
+ result.CurrentValue.Should().BeLessThan(result.Shares * result.CostBasisPerShare);
+ }
+
+ // Tests that current value calculation is correct (shares Γ current price).
+ [Fact]
+ public async Task GetInvestmentDetails_CalculatesCurrentValueCorrectly()
+ {
+ var investmentId = 1; // Apple investment
+
+ var result = await _service.GetInvestmentDetails(investmentId);
+
+ result.Should().NotBeNull();
+ var expectedCurrentValue = result!.Shares * result.CurrentPrice;
+ result.CurrentValue.Should().Be(expectedCurrentValue);
+ }
+
+ // Tests that total gain/loss calculation is correct (current value - total cost).
+ [Fact]
+ public async Task GetInvestmentDetails_CalculatesTotalGainLossCorrectly()
+ {
+ var investmentId = 1; // Apple investment
+
+ var result = await _service.GetInvestmentDetails(investmentId);
+
+ result.Should().NotBeNull();
+ var totalCost = result!.Shares * result.CostBasisPerShare;
+ var expectedGainLoss = result.CurrentValue - totalCost;
+ result.TotalGainLoss.Should().Be(expectedGainLoss);
+ }
+
+ // Tests that investments held for more than 365 days are classified as "Long Term".
+ [Theory]
+ [InlineData(1)] // Apple - Long term (400+ days)
+ [InlineData(3)] // Google - Long term (600+ days)
+ public async Task GetInvestmentDetails_LongTermInvestments_ReturnsLongTerm(int investmentId)
+ {
+ var result = await _service.GetInvestmentDetails(investmentId);
+
+ result.Should().NotBeNull();
+ result!.Term.Should().Be("Long Term");
+ }
+
+ // Tests that investments held for 365 days or less are classified as "Short Term".
+ [Theory]
+ [InlineData(2)] // Microsoft - Short term (200 days)
+ [InlineData(4)] // Tesla - Short term (150 days)
+ [InlineData(5)] // Meta - Short term (30 days)
+ public async Task GetInvestmentDetails_ShortTermInvestments_ReturnsShortTerm(int investmentId)
+ {
+ var result = await _service.GetInvestmentDetails(investmentId);
+
+ result.Should().NotBeNull();
+ result!.Term.Should().Be("Short Term");
+ }
+}
diff --git a/CodingExercise.Tests/InvestmentsControllersTests.cs b/CodingExercise.Tests/InvestmentsControllersTests.cs
new file mode 100644
index 00000000..27e24789
--- /dev/null
+++ b/CodingExercise.Tests/InvestmentsControllersTests.cs
@@ -0,0 +1,199 @@
+ο»Ώusing Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using FluentAssertions;
+using Moq;
+using Xunit;
+using System.Net;
+using System.Text.Json;
+using CodingExercise.Controllers;
+using CodingExercise.Services;
+using CodingExercise.Models;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CodingExercise.Tests;
+
+public class InvestmentsControllerTests
+{
+ private readonly Mock _mockService;
+ private readonly Mock> _mockLogger;
+ private readonly InvestmentsController _controller;
+
+ public InvestmentsControllerTests()
+ {
+ _mockService = new Mock();
+ _mockLogger = new Mock>();
+ _controller = new InvestmentsController(_mockService.Object, _mockLogger.Object);
+ }
+
+ // Tests the happy path where a valid userId returns investment data.
+ // Should return HTTP 200 with a list of investment summaries.
+ [Fact]
+ public async Task GetUserInvestments_ValidUserId_ReturnsOkWithInvestments()
+ {
+ var userId = "user1";
+ var expectedInvestments = new List
+ {
+ new() { Id = 1, Name = "Apple" },
+ new() { Id = 2, Name = "Microsoft" }
+ };
+
+ _mockService.Setup(s => s.GetUserInvestments(userId))
+ .ReturnsAsync(expectedInvestments);
+
+ var result = await _controller.GetUserInvestments(userId);
+
+ result.Result.Should().BeOfType();
+ var okResult = (OkObjectResult)result.Result!;
+ okResult.Value.Should().BeEquivalentTo(expectedInvestments);
+ }
+
+ // Tests how the controller handles an empty string userId.
+ // Should return a bad request stating User ID is required.
+ [Fact]
+ public async Task GetUserInvestments_EmptyUserId_ReturnsBadRequest()
+ {
+ var userId = "";
+
+ var result = await _controller.GetUserInvestments(userId);
+
+ result.Result.Should().BeOfType();
+ var badRequestResult = (BadRequestObjectResult)result.Result!;
+ badRequestResult.Value.Should().Be("User ID is required");
+ }
+
+ // Tests how the controller handles whitespace-only userId.
+ // Should return a bad request since whitespace is not a valid user ID.
+ [Fact]
+ public async Task GetUserInvestments_WhitespaceUserId_ReturnsBadRequest()
+ {
+ var userId = " ";
+
+ var result = await _controller.GetUserInvestments(userId);
+
+ result.Result.Should().BeOfType();
+ }
+
+ // Tests how the controller handles unexpected service exceptions.
+ // Should catch the exception and return HTTP 500 without exposing internal details.
+ [Fact]
+ public async Task GetUserInvestments_ServiceThrowsException_ReturnsInternalServerError()
+ {
+ var userId = "user1";
+ _mockService.Setup(s => s.GetUserInvestments(userId))
+ .ThrowsAsync(new Exception("Database error"));
+
+ var result = await _controller.GetUserInvestments(userId);
+
+ result.Result.Should().BeOfType();
+ var objectResult = (ObjectResult)result.Result!;
+ objectResult.StatusCode.Should().Be(500);
+ }
+
+ // Tests the happy path for getting detailed investment information.
+ // Should return HTTP 200 with complete investment details including calculations.
+ [Fact]
+ public async Task GetInvestmentDetails_ValidInvestmentId_ReturnsOkWithDetails()
+ {
+ var investmentId = 1;
+ var expectedDetails = new InvestmentDetails
+ {
+ Id = 1,
+ Name = "Apple",
+ Shares = 100,
+ CostBasisPerShare = 150.00m,
+ CurrentValue = 17550.00m,
+ CurrentPrice = 175.50m,
+ Term = "Long Term",
+ TotalGainLoss = 2550.00m
+ };
+
+ _mockService.Setup(s => s.GetInvestmentDetails(investmentId))
+ .ReturnsAsync(expectedDetails);
+
+ var result = await _controller.GetInvestmentDetails(investmentId);
+
+ result.Result.Should().BeOfType();
+ var okResult = (OkObjectResult)result.Result!;
+ okResult.Value.Should().BeEquivalentTo(expectedDetails);
+ }
+
+ // Tests validation for investment ID of zero.
+ // Should return HTTP 400 since investment IDs must be positive numbers.
+ [Fact]
+ public async Task GetInvestmentDetails_InvalidInvestmentId_ReturnsBadRequest()
+ {
+ var investmentId = 0;
+
+ var result = await _controller.GetInvestmentDetails(investmentId);
+
+ result.Result.Should().BeOfType();
+ var badRequestResult = (BadRequestObjectResult)result.Result!;
+ badRequestResult.Value.Should().Be("Investment ID must be greater than 0");
+ }
+
+ // Tests validation for negative investment IDs.
+ // Should return HTTP 400 since negative numbers are not valid investment IDs.
+ [Fact]
+ public async Task GetInvestmentDetails_NegativeInvestmentId_ReturnsBadRequest()
+ {
+ var investmentId = -1;
+
+ var result = await _controller.GetInvestmentDetails(investmentId);
+
+ result.Result.Should().BeOfType();
+ }
+
+ // Tests behavior when an investment ID doesn't exist in the system.
+ // Should return HTTP 404 with a descriptive message about the missing investment.
+ [Fact]
+ public async Task GetInvestmentDetails_InvestmentNotFound_ReturnsNotFound()
+ {
+ var investmentId = 999;
+ _mockService.Setup(s => s.GetInvestmentDetails(investmentId))
+ .ReturnsAsync((InvestmentDetails?)null);
+
+ var result = await _controller.GetInvestmentDetails(investmentId);
+
+ result.Result.Should().BeOfType();
+ var notFoundResult = (NotFoundObjectResult)result.Result!;
+ notFoundResult.Value.Should().Be($"Investment with ID {investmentId} not found for user");
+ }
+
+ // Tests exception handling for the investment details endpoint.
+ // Should catch service exceptions and return HTTP 500 with a safe error message.
+ [Fact]
+ public async Task GetInvestmentDetails_ServiceThrowsException_ReturnsInternalServerError()
+ {
+ var investmentId = 1;
+ _mockService.Setup(s => s.GetInvestmentDetails(investmentId))
+ .ThrowsAsync(new Exception("Database error"));
+
+ var result = await _controller.GetInvestmentDetails(investmentId);
+
+ result.Result.Should().BeOfType();
+ var objectResult = (ObjectResult)result.Result!;
+ objectResult.StatusCode.Should().Be(500);
+ }
+
+ // Tests the health check endpoint to ensure the API is operational.
+ // Should always return HTTP 200 with status information for monitoring purposes.
+ [Fact]
+ public void HealthCheck_Always_ReturnsOkWithHealthStatus()
+ {
+ var result = _controller.HealthCheck();
+
+ result.Result.Should().BeOfType();
+ var okResult = (OkObjectResult)result.Result!;
+
+ var healthStatus = okResult.Value;
+ healthStatus.Should().NotBeNull();
+
+ // Use reflection to check the anonymous object properties
+ var statusProperty = healthStatus!.GetType().GetProperty("Status");
+ statusProperty?.GetValue(healthStatus).Should().Be("Healthy");
+
+ var serviceProperty = healthStatus.GetType().GetProperty("Service");
+ serviceProperty?.GetValue(healthStatus).Should().Be("Investment Performance API");
+ }
+}
diff --git a/ReadMe-old.md b/ReadMe-old.md
new file mode 100644
index 00000000..ad2afefb
--- /dev/null
+++ b/ReadMe-old.md
@@ -0,0 +1,28 @@
+# Coding Exercise
+> This repository holds coding exercises for candidates going through the hiring process.
+
+You should have been assigned one of the coding exercises in this list. More details can be found in the specific md file for that assignment.
+
+Instructions: Fork this repository, do the assigned work, and submit a pull request for review.
+
+[Investment Performance Web API](InvestmentPerformanceWebAPI.md#investment-performance-web-api)
+
+[Online Ordering SQL](OnlineOrderingSQL.md#online-ordering)
+
+# License
+
+```
+Copyright 2021 Nuix
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
\ No newline at end of file
diff --git a/ReadMe.md b/ReadMe.md
index ad2afefb..c21d07c8 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -1,28 +1,156 @@
-# Coding Exercise
-> This repository holds coding exercises for candidates going through the hiring process.
+# Investment Performance Web API - Implementation Summary
-You should have been assigned one of the coding exercises in this list. More details can be found in the specific md file for that assignment.
+## β COMPLETED SOLUTION
-Instructions: Fork this repository, do the assigned work, and submit a pull request for review.
+I have successfully created a **production-ready Investment Performance Web API** that meets all the requirements from the coding exercise. The solution is fully functional and tested.
-[Investment Performance Web API](InvestmentPerformanceWebAPI.md#investment-performance-web-api)
+## π― REQUIREMENTS FULFILLED
-[Online Ordering SQL](OnlineOrderingSQL.md#online-ordering)
+### β **Two Main API Endpoints**
-# License
+1. **GET** `/api/investments/user/{userId}` - Returns investment IDs and names for a user
+2. **GET** `/api/investments/investment/{investmentId}` - Returns complete details for a single investment
+### β **Business Logic Implemented**
+
+- **Current Value**: Shares Γ Current Price per Share β
+- **Total Gain/Loss**: Current Value - (Shares Γ Cost Basis) β
+- **Term Classification**: β€365 days = "Short Term", >365 days = "Long Term" β
+- **Gain/Loss Percentage**: (Total Gain/Loss / Total Cost) Γ 100 β
+
+### β **Production-Ready Features**
+
+- **Exception Handling**: Comprehensive try-catch blocks with proper error responses β
+- **Logging**: Structured logging throughout the application β
+- **Input Validation**: Data annotations and controller-level validation β
+- **HTTP Status Codes**: Proper 200, 400, 404, 500 responses β
+- **API Documentation**: Swagger integration β
+- **CORS Support**: Cross-origin request handling β
+- **Health Check**: Monitoring endpoint β
+
+
+## Assumptions Made
+1. **Authentication/Authorization**: Not implemented for this demo
+2. **Database**: Using in-memory data - would use SQL Server/Entity Framework in production
+3. **Market Data**: Using static prices - would integrate with real-time market data APIs
+4. **Caching**: Not implemented for this demo
+5. **Rate Limiting**: Not implemented for this demo
+
+## π§ TECHNOLOGY STACK
+
+- **Framework**: ASP.NET Core 9.0 (Latest)
+- **Language**: C# 12
+- **Documentation**: Swagger 3.0
+- **Testing**: xUnit
+
+## π PROJECT STRUCTURE
+
+```
+CodingExercise/
+βββ src/
+β βββ Controllers/
+β β βββ InvestmentsController.cs # API endpoints with full validation
+β βββ Models/
+β β βββ Investment.cs # Core entity model
+β β βββ InvestmentSummary.cs # List response DTO
+β β βββ InvestmentDetails.cs # Detail response DTO
+β βββ Services/
+β β βββ IInvestmentService.cs # Service interface
+β β βββ InvestmentService.cs # Business logic implementation
+β βββ Program.cs # Application configuration
+β βββ appsettings.json # Logging configuration
+β βββ CodingExercise.csproj
+βββ CodingExercise.Tests/
+ βββ InvestmentsControllersTests.cs # Controller unit tests
+ βββ InvestmentServiceTests.cs # Service unit tests
+ βββ CodingExercise.Tests.csproj
+```
+
+## π§ͺ SAMPLE DATA INCLUDED
+
+The API includes sample data demonstrating:
+
+- **Multiple Users**: user1 (4 investments), user2 (1 investment)
+- **Various Scenarios**: Gains, losses, short-term, long-term
+- **Sample Companies**: Apple, Microsoft, Tesla, Google, Meta
+
+## π HOW TO RUN
+
+### **Start the API:**
+```bash
+cd src
+dotnet restore
+dotnet run
```
-Copyright 2021 Nuix
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+The API will start on `http://localhost:5000`
+
+### **Access Swagger Documentation:**
+Visit: `http://localhost:5000` in your browser for interactive API documentation
+
+### **Test the API:**
+You can test using:
+1. **Swagger UI** use **user1 or user2** for user ids and **1-5** for investment ids
+2. **Command line** with the curl examples below
+
+### β Health Check
+```bash
+curl -X GET http://localhost:5000/api/investments/health -H "accept: application/json"
+Response: {"status":"Healthy","timestamp":"2025-11-10T22:10:28.5681609Z","service":"Investment Performance API"}
+```
+
+### β Get User1 Investments
+```bash
+curl -X GET http://localhost:5000/api/investments/user/user1 -H "accept: application/json"
+Response: [{"id":1,"name":"Apple"},{"id":2,"name":"Microsoft"},{"id":3,"name":"Google"},{"id":5,"name":"Meta"}]
+```
+
+### β Get Apple Stock Details (Profitable Investment)
+```bash
+curl -X GET "http://localhost:5000/api/investments/investment/1" -H "accept: application/json"
+Response: {
+ "id":1,
+ "name":"Apple",
+ "shares":100,
+ "costBasisPerShare":150.00,
+ "currentValue":17550.00,
+ "currentPrice":175.50,
+ "term":"Long Term",
+ "totalGainLoss":2550.00,
+}
+```
+
+### β Get Tesla Details (Loss Position)
+```bash
+curl -X GET "http://localhost:5000/api/investments/investment/4" -H "accept: application/json"
+Response: {
+ "id":4,
+ "name":"Tesla",
+ "shares":25,
+ "costBasisPerShare":800.00,
+ "currentValue":18750.00,
+ "currentPrice":750.00,
+ "term":"Short Term",
+ "totalGainLoss":-1250.00,
+}
+```
+
+
+## π§ͺ HOW TO RUN TESTS
+
+```bash
+cd CodingExercise.Tests
+dotnet restore
+dotnet test
+```
+
+You should see 27 successful tests.
+
+## π ERROR HANDLING EXAMPLES
- http://www.apache.org/licenses/LICENSE-2.0
+The API properly handles error cases:
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-```
\ No newline at end of file
+- **Invalid User**: Returns empty array (because my mock data does not have a user "table" and the user id is just attached to the investment data so if no investments have the user id specified it will give an empty response. Could also do a 404 if the data were different.)
+- **Invalid Investment ID**: Returns 404 with descriptive message
+- **Missing Parameters**: Returns 400 with validation errors
+- **Server Errors**: Returns 500 with generic message (no sensitive data exposed)
\ No newline at end of file
diff --git a/src/CodingExercise.csproj b/src/CodingExercise.csproj
new file mode 100644
index 00000000..0126135a
--- /dev/null
+++ b/src/CodingExercise.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net9.0
+ enable
+ enable
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
diff --git a/src/Controllers/InvestmentsController.cs b/src/Controllers/InvestmentsController.cs
new file mode 100644
index 00000000..33f44583
--- /dev/null
+++ b/src/Controllers/InvestmentsController.cs
@@ -0,0 +1,110 @@
+using Microsoft.AspNetCore.Mvc;
+using CodingExercise.Models;
+using CodingExercise.Services;
+using System.ComponentModel.DataAnnotations;
+
+namespace CodingExercise.Controllers
+{
+ ///
+ /// Controller for managing investment operations and performance data
+ ///
+ [ApiController]
+ [Route("api/[controller]")]
+ [Produces("application/json")]
+ public class InvestmentsController : ControllerBase
+ {
+ private readonly IInvestmentService _investmentService;
+ private readonly ILogger _logger;
+
+ public InvestmentsController(IInvestmentService investmentService, ILogger logger)
+ {
+ _investmentService = investmentService;
+ _logger = logger;
+ }
+
+ ///
+ /// Get a list of current investments for the user
+ ///
+ /// The user identifier
+ /// List of investment summaries containing ID and name
+ [HttpGet("user/{userId}")]
+ [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetUserInvestments(
+ [Required] string userId)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(userId))
+ {
+ _logger.LogWarning("GetUserInvestments called with empty userId");
+ return BadRequest("User ID is required");
+ }
+
+ var investments = await _investmentService.GetUserInvestments(userId);
+ return Ok(investments);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error occurred while getting investments for user: {UserId}", userId);
+ return StatusCode(StatusCodes.Status500InternalServerError,
+ "An error occurred while processing your request");
+ }
+ }
+
+ ///
+ /// Get details for a user's specific investment
+ ///
+ /// The investment identifier
+ /// Detailed investment performance data
+ [HttpGet("investment/{investmentId}")]
+ [ProducesResponseType(typeof(InvestmentDetails), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> GetInvestmentDetails(
+ [Required][Range(1, int.MaxValue)] int investmentId)
+ {
+ try
+ {
+ if (investmentId <= 0)
+ {
+ _logger.LogWarning("GetInvestmentDetails called with invalid investmentId: {InvestmentId}", investmentId);
+ return BadRequest("Investment ID must be greater than 0");
+ }
+
+ var investmentDetails = await _investmentService.GetInvestmentDetails(investmentId);
+
+ if (investmentDetails == null)
+ {
+ _logger.LogInformation("Investment not found for user: investmentId: {InvestmentId}", investmentId);
+ return NotFound($"Investment with ID {investmentId} not found for user");
+ }
+
+ return Ok(investmentDetails);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error occurred while getting investment details for investmentId: {InvestmentId}", investmentId);
+ return StatusCode(StatusCodes.Status500InternalServerError,
+ "An error occurred while processing your request");
+ }
+ }
+
+ ///
+ /// Health check endpoint for the investments API
+ ///
+ /// API health status
+ [HttpGet("health")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public ActionResult