Skip to content
Merged
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
19 changes: 19 additions & 0 deletions MySupplyChain.API/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,23 @@ public async Task<IActionResult> DeleteAccount()
await mediator.Send(new DeleteAccountCommand(userId));
return NoContent();
}

/// <summary>
/// Update user's username (display name)
/// </summary>
[Authorize]
[HttpPut("username")]
[ProducesResponseType(typeof(object), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult> UpdateUsername([FromBody] UpdateUsernameDto dto)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(userId)) return Unauthorized();

var token = await mediator.Send(new MySupplyChain.Application.Auth.Commands.UpdateUsername.UpdateUsernameCommand(userId, dto.NewUsername));
return Ok(new { Token = token, Message = "Username updated successfully" });
}
}

public record UpdateUsernameDto(string NewUsername);
30 changes: 14 additions & 16 deletions MySupplyChain.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,13 @@
});

builder.Services.AddDataProtection();
builder.Services.AddHttpContextAccessor();

builder.Services.AddIdentityCore<MySupplyChain.Domain.Entities.User>(options =>
{
// Require unique emails for all accounts
options.User.RequireUniqueEmail = true;

//Add spaces to the allowed user name characters
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+ ";

Expand Down Expand Up @@ -149,23 +153,17 @@
// ─── Seed Database ─────────────────────────────────────────────────────────
if (!IsIntegrationTestRun)
{
using (var scope = app.Services.CreateScope())
using var scope = app.Services.CreateScope();
var services = scope.ServiceProvider;
try
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<MySupplyChain.Infrastructure.Persistence.ApplicationDbContext>();
await context.Database.MigrateAsync();

var seederLogger = services.GetRequiredService<ILogger<Program>>();
var csvPath = Path.Combine(app.Environment.ContentRootPath, "..", "data", "Retail-Supply-Chain-Sales-Dataset.csv");
await MySupplyChain.Infrastructure.Persistence.CsvDatabaseSeeder.SeedAsync(context, csvPath, seederLogger);
}
catch (Exception ex)
{
var seederLogger = services.GetRequiredService<ILogger<Program>>();
seederLogger.LogError(ex, "An error occurred while seeding the database.");
}
var context = services.GetRequiredService<MySupplyChain.Infrastructure.Persistence.ApplicationDbContext>();
await context.Database.MigrateAsync();
}
catch (Exception ex)
{
var seederLogger = services.GetRequiredService<ILogger<Program>>();
seederLogger.LogError(ex, "An error occurred while migrating the database.");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ public class DeleteAccountCommandHandler(IApplicationDbContext context, IAuthSer
{
public async Task Handle(DeleteAccountCommand request, CancellationToken cancellationToken)
{
// 1. Wipe business data first
// In a real app, we'd filter these by UserId
// 1. Wipe business data first (scoped automatically by global query filters)
context.SalesHistories.RemoveRange(context.SalesHistories);
context.Orders.RemoveRange(context.Orders);
context.Products.RemoveRange(context.Products);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using MediatR;
using MySupplyChain.Application.Common.Interfaces;

namespace MySupplyChain.Application.Auth.Commands.UpdateUsername;

public record UpdateUsernameCommand(string UserId, string NewUsername) : IRequest<string>;

public class UpdateUsernameCommandHandler(IAuthService authService) : IRequestHandler<UpdateUsernameCommand, string>
{
public async Task<string> Handle(UpdateUsernameCommand request, CancellationToken cancellationToken)
{
return await authService.UpdateUsernameAsync(request.UserId, request.NewUsername);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using MySupplyChain.Application.Common.Interfaces;

namespace MySupplyChain.Application.Auth.Commands.WipeUserData;
Expand All @@ -10,8 +9,8 @@ public class WipeUserDataCommandHandler(IApplicationDbContext context) : IReques
{
public async Task Handle(WipeUserDataCommand request, CancellationToken cancellationToken)
{
// In a real multi-tenant app, filter by UserId.
// For this single-user demo/prototype, we wipe the global tables.
// The global query filters automatically scope these collections
// to the current authenticated user's data.

context.SalesHistories.RemoveRange(context.SalesHistories);
context.Orders.RemoveRange(context.Orders);
Expand Down
5 changes: 5 additions & 0 deletions MySupplyChain.Application/Common/Interfaces/IAuthService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ public interface IAuthService
/// </summary>
Task DeleteAccountAsync(string userId);

/// <summary>
/// Updates the user's username and returns a new JWT token
/// </summary>
Task<string> UpdateUsernameAsync(string userId, string newUsername);

string GenerateJwtToken(User user);
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public async Task<IEnumerable<OrderDto>> Handle(GetOrdersQuery request, Cancella
Id = o.Id,
OrderNumber = o.OrderNumber,
Date = o.OrderDate.ToString("MMM dd, HH:mm"),
Customer = o.Customer.Name,
Customer = o.Customer?.Name ?? "Unknown Customer",
Items = o.Items.Sum(i => i.Quantity),
Status = o.Status.ToString().ToLower(),
Total = o.TotalAmount.ToString("C")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using CsvHelper.Configuration;
using MediatR;
using Microsoft.EntityFrameworkCore;
using MySupplyChain.Application.Common.Exceptions;
using MySupplyChain.Application.Common.Interfaces;
using MySupplyChain.Domain.Entities;
using System.Globalization;
Expand All @@ -19,7 +18,7 @@ public async Task<ImportSummaryDto> Handle(ImportSalesHistoryCommand request, Ca
// Harden cache against duplicate SKUs in DB
// We fetch the list first to avoid untranslatable GroupBy issues in EF Core
var productsList = await context.Products
.Where(p => p.Sku != null && p.Sku != "")
.Where(p => p.Sku != "")
.ToListAsync(cancellationToken);

var productsCache = productsList
Expand All @@ -28,7 +27,7 @@ public async Task<ImportSummaryDto> Handle(ImportSalesHistoryCommand request, Ca

// Cache customers by email to avoid duplicates
var customersList = await context.Customers
.Where(c => c.Email != null)
.Where(c => c.Email != "")
.ToListAsync(cancellationToken);

var customersCache = customersList
Expand Down Expand Up @@ -89,7 +88,10 @@ public async Task<ImportSummaryDto> Handle(ImportSalesHistoryCommand request, Ca
if (!string.IsNullOrEmpty(request.ProductPriceColumn) && headers.Contains(request.ProductPriceColumn))
{
var priceStr = csv.GetField<string>(request.ProductPriceColumn);
decimal.TryParse(priceStr, out price);
if (!decimal.TryParse(priceStr, out price))
{
price = 0m;
}
}

product = new Product
Expand Down
6 changes: 3 additions & 3 deletions MySupplyChain.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ public async Task<ForecastResult> Forecast_Fallback()
private static float[] GenerateSeries(Random rng, int count)
{
var series = new float[count];
float base_ = 50f;
var baseValue = 50f;
for (int i = 0; i < count; i++)
{
// Trend + weekly seasonality + noise
base_ += rng.NextSingle() * 0.2f - 0.1f;
baseValue += rng.NextSingle() * 0.2f - 0.1f;
var seasonal = 5f * MathF.Sin(2f * MathF.PI * i / 7f);
var noise = (float)(rng.NextDouble() * 4 - 2);
series[i] = MathF.Max(0, base_ + seasonal + noise);
series[i] = MathF.Max(0, baseValue + seasonal + noise);
}
return series;
}
Expand Down
61 changes: 33 additions & 28 deletions MySupplyChain.Domain/Entities/EntityBase.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
/*
* Author: Austin Chima
* Base class for all domain entities.
*/

namespace MySupplyChain.Domain.Entities;

/// <summary>
/// Abstract base class for all domain entities with common properties
/// </summary>

public abstract class EntityBase
{
/// <summary>
/// Unique identifier for the entity
/// </summary>
public int Id { get; set; }

/// <summary>
/// When the entity was created (UTC)
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

/// <summary>
/// When the entity was last updated (UTC)
/// </summary>
public DateTime? UpdatedAt { get; set; }
}
/*
* Author: Austin Chima
* Base class for all domain entities.
*/

namespace MySupplyChain.Domain.Entities;

/// <summary>
/// Abstract base class for all domain entities with common properties
/// </summary>

public abstract class EntityBase
{
/// <summary>
/// Unique identifier for the entity
/// </summary>
public int Id { get; set; }

/// <summary>
/// When the entity was created (UTC)
/// </summary>
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

/// <summary>
/// When the entity was last updated (UTC)
/// </summary>
public DateTime? UpdatedAt { get; set; }

/// <summary>
/// The owner user identifier for multi-tenancy
/// </summary>
public string? UserId { get; set; }
}
2 changes: 1 addition & 1 deletion MySupplyChain.Domain/Entities/Order.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ public class Order : EntityBase
public OrderStatus Status { get; set; }
public decimal TotalAmount { get; set; }

public Customer Customer { get; set; } = null!;
public Customer? Customer { get; set; } = null!;
public ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();
}
Loading
Loading