-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from neozhu/feature/stock
Tutorial: Add Stock Entity and Features for Inventory Management
- Loading branch information
Showing
58 changed files
with
3,091 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using CleanAspire.Application.Common.Models; | ||
using CleanAspire.Application.Features.Stocks.Commands; | ||
using CleanAspire.Application.Features.Stocks.DTOs; | ||
using CleanAspire.Application.Features.Stocks.Queryies; | ||
using Mediator; | ||
using Microsoft.AspNetCore.Mvc; | ||
|
||
namespace CleanAspire.Api.Endpoints; | ||
|
||
public class StockEndpointRegistrar(ILogger<ProductEndpointRegistrar> logger) : IEndpointRegistrar | ||
{ | ||
public void RegisterRoutes(IEndpointRouteBuilder routes) | ||
{ | ||
var group = routes.MapGroup("/stocks").WithTags("stocks").RequireAuthorization(); | ||
|
||
// Dispatch stock | ||
group.MapPost("/dispatch", ([FromServices] IMediator mediator, [FromBody] StockDispatchingCommand command) => mediator.Send(command)) | ||
.Produces<Unit>(StatusCodes.Status200OK) | ||
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity) | ||
.ProducesProblem(StatusCodes.Status400BadRequest) | ||
.ProducesProblem(StatusCodes.Status500InternalServerError) | ||
.WithSummary("Dispatch stock") | ||
.WithDescription("Dispatches a specified quantity of stock from a location."); | ||
|
||
// Receive stock | ||
group.MapPost("/receive", ([FromServices] IMediator mediator, [FromBody] StockReceivingCommand command) => mediator.Send(command)) | ||
.Produces<Unit>(StatusCodes.Status200OK) | ||
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity) | ||
.ProducesProblem(StatusCodes.Status400BadRequest) | ||
.ProducesProblem(StatusCodes.Status500InternalServerError) | ||
.WithSummary("Receive stock") | ||
.WithDescription("Receives a specified quantity of stock into a location."); | ||
|
||
// Get stocks with pagination | ||
group.MapPost("/pagination", ([FromServices] IMediator mediator, [FromBody] StocksWithPaginationQuery query) => mediator.Send(query)) | ||
.Produces<PaginatedResult<StockDto>>(StatusCodes.Status200OK) | ||
.ProducesProblem(StatusCodes.Status400BadRequest) | ||
.ProducesProblem(StatusCodes.Status500InternalServerError) | ||
.WithSummary("Get stocks with pagination") | ||
.WithDescription("Returns a paginated list of stocks based on search keywords, page size, and sorting options."); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 26 additions & 1 deletion
27
src/CleanAspire.Application/Common/Interfaces/IApplicationDbContext.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,35 @@ | ||
namespace CleanAspire.Application.Common.Interfaces; | ||
|
||
/// <summary> | ||
/// Represents the application database context interface. | ||
/// </summary> | ||
public interface IApplicationDbContext | ||
{ | ||
/// <summary> | ||
/// Gets or sets the Products DbSet. | ||
/// </summary> | ||
DbSet<Product> Products { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the AuditTrails DbSet. | ||
/// </summary> | ||
DbSet<AuditTrail> AuditTrails { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the Tenants DbSet. | ||
/// </summary> | ||
DbSet<Tenant> Tenants { get; set; } | ||
Task<int> SaveChangesAsync(CancellationToken cancellationToken=default); | ||
|
||
/// <summary> | ||
/// Gets or sets the Stocks DbSet. | ||
/// </summary> | ||
DbSet<Stock> Stocks { get; set; } | ||
|
||
/// <summary> | ||
/// Saves all changes made in this context to the database. | ||
/// </summary> | ||
/// <param name="cancellationToken">A CancellationToken to observe while waiting for the task to complete.</param> | ||
/// <returns>A task that represents the asynchronous save operation. The task result contains the number of state entries written to the database.</returns> | ||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
src/CleanAspire.Application/Features/Products/Queries/ProductsWithPaginationQuery.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
src/CleanAspire.Application/Features/Stocks/Commands/StockDispatchingCommand.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using CleanAspire.Application.Pipeline; | ||
|
||
namespace CleanAspire.Application.Features.Stocks.Commands; | ||
public record StockDispatchingCommand : IFusionCacheRefreshRequest<Unit>, IRequiresValidation | ||
{ | ||
public string ProductId { get; init; } = string.Empty; | ||
public int Quantity { get; init; } | ||
public string Location { get; init; } = string.Empty; | ||
public IEnumerable<string>? Tags => new[] { "stocks" }; | ||
} | ||
public class StockDispatchingCommandHandler : IRequestHandler<StockDispatchingCommand, Unit> | ||
{ | ||
private readonly IApplicationDbContext _context; | ||
|
||
public StockDispatchingCommandHandler(IApplicationDbContext context) | ||
{ | ||
_context = context ?? throw new ArgumentNullException(nameof(context)); | ||
} | ||
|
||
public async ValueTask<Unit> Handle(StockDispatchingCommand request, CancellationToken cancellationToken) | ||
{ | ||
// Validate that the product exists | ||
var product = await _context.Products | ||
.FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); | ||
|
||
if (product == null) | ||
{ | ||
throw new KeyNotFoundException($"Product with Product ID '{request.ProductId}' was not found."); | ||
} | ||
|
||
// Check if the stock record exists for the given ProductId and Location | ||
var existingStock = await _context.Stocks | ||
.FirstOrDefaultAsync(s => s.ProductId == request.ProductId && s.Location == request.Location, cancellationToken); | ||
|
||
if (existingStock == null) | ||
{ | ||
throw new KeyNotFoundException($"No stock record found for Product ID '{request.ProductId}' at Location '{request.Location}'."); | ||
} | ||
|
||
// Validate that the stock quantity is sufficient | ||
if (existingStock.Quantity < request.Quantity) | ||
{ | ||
throw new InvalidOperationException($"Insufficient stock quantity. Available: {existingStock.Quantity}, Requested: {request.Quantity}"); | ||
} | ||
|
||
// Reduce the stock quantity | ||
existingStock.Quantity -= request.Quantity; | ||
|
||
// If stock quantity is zero, remove the stock record | ||
if (existingStock.Quantity == 0) | ||
{ | ||
_context.Stocks.Remove(existingStock); | ||
} | ||
else | ||
{ | ||
// Update the stock record | ||
_context.Stocks.Update(existingStock); | ||
} | ||
|
||
// Save changes to the database | ||
await _context.SaveChangesAsync(cancellationToken); | ||
|
||
return Unit.Value; | ||
} | ||
|
||
} |
70 changes: 70 additions & 0 deletions
70
src/CleanAspire.Application/Features/Stocks/Commands/StockReceivingCommand.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using CleanAspire.Application.Common.Interfaces; | ||
using CleanAspire.Application.Features.Stocks.DTOs; | ||
using CleanAspire.Application.Pipeline; | ||
|
||
namespace CleanAspire.Application.Features.Stocks.Commands; | ||
public record StockReceivingCommand : IFusionCacheRefreshRequest<Unit>, IRequiresValidation | ||
{ | ||
public string ProductId { get; init; } = string.Empty; | ||
public int Quantity { get; init; } | ||
public string Location { get; init; } = string.Empty; | ||
public IEnumerable<string>? Tags => new[] { "stocks" }; | ||
} | ||
public class StockReceivingCommandHandler : IRequestHandler<StockReceivingCommand, Unit> | ||
{ | ||
private readonly IApplicationDbContext _context; | ||
|
||
public StockReceivingCommandHandler(IApplicationDbContext context) | ||
{ | ||
_context = context ?? throw new ArgumentNullException(nameof(context)); | ||
} | ||
|
||
public async ValueTask<Unit> Handle(StockReceivingCommand request, CancellationToken cancellationToken) | ||
{ | ||
// Validate that the product exists | ||
var product = await _context.Products | ||
.FirstOrDefaultAsync(p => p.Id == request.ProductId, cancellationToken); | ||
|
||
if (product == null) | ||
{ | ||
throw new KeyNotFoundException($"Product with Product ID '{request.ProductId}' was not found."); | ||
} | ||
|
||
// Check if the stock record already exists for the given ProductId and Location | ||
var existingStock = await _context.Stocks | ||
.FirstOrDefaultAsync(s => s.ProductId == request.ProductId && s.Location == request.Location, cancellationToken); | ||
|
||
if (existingStock != null) | ||
{ | ||
// If the stock record exists, update the quantity | ||
existingStock.Quantity += request.Quantity; | ||
_context.Stocks.Update(existingStock); | ||
} | ||
else | ||
{ | ||
// If no stock record exists, create a new one | ||
var newStockEntry = new Stock | ||
{ | ||
ProductId = request.ProductId, | ||
Location = request.Location, | ||
Quantity = request.Quantity, | ||
}; | ||
|
||
_context.Stocks.Add(newStockEntry); | ||
} | ||
|
||
// Save changes to the database | ||
await _context.SaveChangesAsync(cancellationToken); | ||
|
||
return Unit.Value; | ||
} | ||
} |
Oops, something went wrong.