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
107 changes: 107 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
###############################################################################
# OS / Editor
###############################################################################
# macOS
.DS_Store
.AppleDouble
.LSOverride

# Windows
Thumbs.db
ehthumbs.db
Desktop.ini

# VS Code (keep shareable settings if you want; ignore local state)
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
!.vscode/tasks.json

# Rider / IntelliJ
.idea/
*.iml

# Visual Studio
.vs/

###############################################################################
# .NET / ASP.NET Core
###############################################################################
bin/
obj/
# User secrets (do NOT commit secrets.json from user profile)
**/secrets.json

# Logs
**/logs/
*.log

# Local environment overrides
appsettings.*.local.json
appsettings.Local.json
appsettings.Development.local.json

# If using SQLite locally, ignore local DB files
*.db
*.db-shm
*.db-wal

# Publish output
**/publish/
**/PublishProfiles/

# Build artifacts
*.user
*.suo
*.cache
*.pdb

###############################################################################
# Node / Vue 3 (Vite)
###############################################################################
# Dependencies
**/node_modules/

# Dist builds (Vite)
**/dist/
**/dist-ssr/
**/.vite/

# Test coverage / reports
**/coverage/
**/.nyc_output/
**/reports/

# Logs
**/npm-debug.log*
**/yarn-debug.log*
**/yarn-error.log*
**/pnpm-debug.log*

# Env files (keep base .env if you use non-secret defaults)
**/.env
**/.env.local
**/.env.*.local
# Optional: keep these if you want environments tracked without secrets
# !client/.env.example
# !server/.env.example

# Cache
**/.cache/
**/.eslintcache
**/.stylelintcache

###############################################################################
# Images / Temp / Misc
###############################################################################
*.tmp
*.temp
*.swp
*.swo

###############################################################################
# KEEP THESE TRACKED (documentation)
###############################################################################
!README.md
!LICENSE
8 changes: 8 additions & 0 deletions InvestmentPerformanceWebAPI.Server/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
This file explains how Visual Studio created the project.

The following steps were used to generate this project:
- Create new ASP\.NET Core Web API project.
- Update project file to add a reference to the frontend project and set SPA properties.
- Update `launchSettings.json` to register the SPA proxy as a startup assembly.
- Add project to the startup projects list.
- Write this file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using InvestmentPerformanceWebAPI.Server.Services;
using InvestmentPerformanceWebAPI.Server.DTOs;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

namespace InvestmentPerformanceWebAPI.Server.Controllers {
[ApiController]
[Route("api/users/{userId:int}/investments")]
public sealed class InvestmentsController : ControllerBase {
private readonly IInvestmentQueryService _service;

public InvestmentsController(IInvestmentQueryService service) {
_service = service;
}

// GET: /api/users/{userId}/investments
[HttpGet]
public async Task<ActionResult<IReadOnlyList<InvestmentListItemDto>>> GetInvestments(
[FromRoute] int userId,
CancellationToken ct) {
IReadOnlyList<InvestmentListItemDto> list = await _service.GetInvestmentsForUserAsync(userId, ct);
return Ok(list);
}

// GET: /api/users/{userId}/investments/{investmentId}
[HttpGet("{investmentId:int}")]
public async Task<ActionResult<InvestmentDetailsDto>> GetInvestmentDetails(
[FromRoute] int userId,
[FromRoute] int investmentId,
CancellationToken ct) {
InvestmentDetailsDto? dto = await _service.GetInvestmentDetailsAsync(userId, investmentId, ct);
if (dto is null) {
return NotFound();
}

return Ok(dto);
}
}
}
43 changes: 43 additions & 0 deletions InvestmentPerformanceWebAPI.Server/Controllers/UsersController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using InvestmentPerformanceWebAPI.Server.Database.Repository;
using InvestmentPerformanceWebAPI.Server.DTOs;
using Microsoft.AspNetCore.Mvc;

namespace InvestmentPerformanceWebAPI.Server.Controllers {
[ApiController]
[Route("api/users")]
public sealed class UsersController : ControllerBase {
private readonly IUserRepository _users;

public UsersController(IUserRepository users) {
_users = users;
}

// GET: /api/users
[HttpGet]
public async Task<ActionResult<IReadOnlyList<UserDto>>> GetAll(CancellationToken ct) {
var list = await _users.GetAllAsync(ct);
var dtos = list
.Select(u => new UserDto {
Id = u.Id,
Name = u.Name
})
.ToList();

return Ok(dtos);
}

// GET: /api/users/{id}
[HttpGet("{id:int}")]
public async Task<ActionResult<UserDto>> GetById([FromRoute] int id, CancellationToken ct) {
var user = await _users.GetByIdAsync(id, ct);
if (user is null) {
return NotFound();
}

return Ok(new UserDto {
Id = user.Id,
Name = user.Name
});
}
}
}
20 changes: 20 additions & 0 deletions InvestmentPerformanceWebAPI.Server/DTOs/InvestmentDetailsDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// DTOs/InvestmentDetailsDto.cs
namespace InvestmentPerformanceWebAPI.Server.DTOs {
public sealed class InvestmentDetailsDto {
public int InvestmentId { get; set; }

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

public decimal NumberOfShares { get; set; }

public decimal CostBasisPerShare { get; set; }

public decimal CurrentPrice { get; set; }

public decimal CurrentValue { get; set; }

public string Term { get; set; } = string.Empty; // "ShortTerm" or "LongTerm"

public decimal TotalGainLoss { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace InvestmentPerformanceWebAPI.Server.DTOs {
public sealed class InvestmentListItemDto {
public int InvestmentId { get; set; }

public string Name { get; set; } = string.Empty;
}
}
7 changes: 7 additions & 0 deletions InvestmentPerformanceWebAPI.Server/DTOs/UserDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace InvestmentPerformanceWebAPI.Server.DTOs {
public sealed class UserDto {
public int Id { get; set; }

public string Name { get; set; } = string.Empty;
}
}
140 changes: 140 additions & 0 deletions InvestmentPerformanceWebAPI.Server/Database/Context/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
using InvestmentPerformanceWebAPI.Server.Database.Models;
using Microsoft.EntityFrameworkCore;

namespace InvestmentPerformanceWebAPI.Server.Database.Context {
public sealed class AppDbContext : DbContext {
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) {
}
public DbSet<Investment> Investments {
get { return Set<Investment>(); }
}

public DbSet<User> Users {
get { return Set<User>(); }
}

public DbSet<Transaction> Transactions {
get { return Set<Transaction>(); }
}

protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<User>(b => {

// Seed users
b.HasData(
new User {
Id = 1,
Name = "Alice"
},
new User {
Id = 2,
Name = "Bob"
}
);
});

// Investments seed (so InvestmentId in Transactions is valid)
modelBuilder.Entity<Investment>(b =>
{
b.HasData(
// Alice's investments
new Investment {
Id = 1,
UserId = 1,
Name = "ACME"
},
new Investment {
Id = 2,
UserId = 1,
Name = "BETA"
},

// Bob's investments
new Investment {
Id = 3,
UserId = 2,
Name = "BETA"
}
);
});

modelBuilder.Entity<Transaction>(b =>
{
b.HasData(
// --- Alice (UserId = 1) ---
new Transaction {
Id = 1,
UserId = 1,
InvestmentId = 1, // ACME (Alice)
Type = TransactionType.Buy,
NumberOfShares = (decimal)10.00,
PricePerShare = (decimal)100.00,
ExecutedAtUtc = new DateTime(2024, 01, 15, 10, 00, 00, DateTimeKind.Utc)
},
// Same investment bought/sold at different prices
new Transaction {
Id = 2,
UserId = 1,
InvestmentId = 1, // ACME (Alice)
Type = TransactionType.Sell,
NumberOfShares = (decimal)3.00,
PricePerShare = (decimal)120.00,
ExecutedAtUtc = new DateTime(2024, 12, 01, 15, 30, 00, DateTimeKind.Utc)
},
new Transaction {
Id = 3,
UserId = 1,
InvestmentId = 1, // ACME (Alice)
Type = TransactionType.Buy,
NumberOfShares = (decimal)5.00,
PricePerShare = (decimal)110.00,
ExecutedAtUtc = new DateTime(2025, 03, 10, 09, 00, 00, DateTimeKind.Utc)
},

new Transaction {
Id = 4,
UserId = 1,
InvestmentId = 2, // BETA (Alice)
Type = TransactionType.Buy,
NumberOfShares = (decimal)8.00,
PricePerShare = (decimal)40.00,
ExecutedAtUtc = new DateTime(2025, 05, 20, 14, 30, 00, DateTimeKind.Utc)
},

// --- Bob (UserId = 2) ---
new Transaction {
Id = 5,
UserId = 2,
InvestmentId = 3, // BETA (Bob)
Type = TransactionType.Buy,
NumberOfShares = (decimal)20.00,
PricePerShare = (decimal)45.00,
ExecutedAtUtc = new DateTime(2025, 02, 20, 14, 00, 00, DateTimeKind.Utc)
},
// Same investment again at different price
new Transaction {
Id = 6,
UserId = 2,
InvestmentId = 3, // BETA (Bob)
Type = TransactionType.Buy,
NumberOfShares = (decimal)10.00,
PricePerShare = (decimal)50.00,
ExecutedAtUtc = new DateTime(2025, 06, 01, 16, 45, 00, DateTimeKind.Utc)
},
// Partial sell
new Transaction {
Id = 7,
UserId = 2,
InvestmentId = 3, // BETA (Bob)
Type = TransactionType.Sell,
NumberOfShares = (decimal)5.00,
PricePerShare = (decimal)55.00,
ExecutedAtUtc = new DateTime(2025, 08, 15, 13, 15, 00, DateTimeKind.Utc)
}
);
});

}
}
}
15 changes: 15 additions & 0 deletions InvestmentPerformanceWebAPI.Server/Database/Models/Investment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;

namespace InvestmentPerformanceWebAPI.Server.Database.Models {
public sealed class Investment {
public int Id { get; set; }

public int UserId { get; set; }

public User User { get; set; } = null!;

[Required]
[MaxLength(64)]
public string Name { get; set; } = string.Empty;
}
}
Loading