From 1a502767bae9ebb5fef737348657e2cf65093a9c Mon Sep 17 00:00:00 2001 From: Haritha Randunu Date: Fri, 9 Oct 2026 14:22:08 +0530 Subject: [PATCH] Add Stock Tracking endpoint to Inventory Adds GET /client/inventory/stock-movements, a unified read-only ledger flattening every GRN line, sale line and return across all products into one shape for the new Stock Tracking tab's table. --- Controllers/InventoryController.cs | 7 +++ DTOs/Inventory/InventoryDtos.cs | 21 ++++++++ Interfaces/Services/IInventoryService.cs | 2 + Services/InventoryService.cs | 61 ++++++++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/Controllers/InventoryController.cs b/Controllers/InventoryController.cs index 17af1e0..ae5d161 100644 --- a/Controllers/InventoryController.cs +++ b/Controllers/InventoryController.cs @@ -170,5 +170,12 @@ namespace Fitness_core_BE.Controllers notes = request.Notes, returnedAt = request.ReturnedAt?.ToString("o") }, HttpContext)); + + [CustomMiddleware] + [TenantResolution] + [RequiresPermission("inventory")] + [HttpGet("stock-movements")] + public async Task>> GetStockMovements() + => Response(await _inventoryService.GetStockMovements(new { }, HttpContext)); } } diff --git a/DTOs/Inventory/InventoryDtos.cs b/DTOs/Inventory/InventoryDtos.cs index 2ebed4f..b0501de 100644 --- a/DTOs/Inventory/InventoryDtos.cs +++ b/DTOs/Inventory/InventoryDtos.cs @@ -85,6 +85,27 @@ namespace Fitness_core_BE.DTOs.Inventory public List Items { get; set; } = []; } + /// One row in the Stock Tracking tab's unified ledger -- every GRN line, sale line + /// and return across every product, flattened into one shape for a single filterable table. + /// Not persisted; built on read from GRNs/Sales/Returns. + public class StockMovementDto + { + /// "GRN" | "Sale" | "Return". + public string Type { get; set; } = string.Empty; + public Guid ProductId { get; set; } + public string ProductName { get; set; } = string.Empty; + /// The GRN/Sale number this line belongs to, or the return's Category label. + public string Reference { get; set; } = string.Empty; + /// Only set for Type == "Return" -- the write-off/return category. + public string? Category { get; set; } + /// Signed: positive for a GRN or a CustomerReturn (stock in), negative for a + /// Sale or any other return category (stock out). + public decimal QuantityChange { get; set; } + /// This line's monetary value -- a GRN/Sale line's LineTotal, or a return's Amount. + public decimal Amount { get; set; } + public DateTime Date { get; set; } + } + public class StockReturnDto { public Guid StockReturnId { get; set; } diff --git a/Interfaces/Services/IInventoryService.cs b/Interfaces/Services/IInventoryService.cs index e4ed98c..b515fb3 100644 --- a/Interfaces/Services/IInventoryService.cs +++ b/Interfaces/Services/IInventoryService.cs @@ -22,5 +22,7 @@ namespace Fitness_core_BE.Interfaces.Services Task CreateStockReturn(object payload, HttpContext httpContext); Task GetStockReturns(object payload, HttpContext httpContext); Task GetStockReturnById(object payload, HttpContext httpContext); + + Task GetStockMovements(object payload, HttpContext httpContext); } } diff --git a/Services/InventoryService.cs b/Services/InventoryService.cs index d20fe43..4260899 100644 --- a/Services/InventoryService.cs +++ b/Services/InventoryService.cs @@ -576,6 +576,67 @@ namespace Fitness_core_BE.Services return MapToDto(stockReturn); } + // --- Stock Tracking (unified ledger, new) --- + + /// Every GRN line, sale line and return across every product, flattened into one + /// shape for the Stock Tracking tab's table. Built from the same three already-tenant- + /// scoped reads the GRN/Sales/Returns tabs use -- no new repository queries needed, and + /// the dataset is small enough (a single gym's "simple inventory") that filtering happens + /// client-side rather than via query params. + public async Task GetStockMovements(object payload, HttpContext httpContext) + { + var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext)); + + var grns = await _inventoryRepository.GetGrns(clientId); + var sales = await _inventoryRepository.GetSales(clientId); + var returns = await _inventoryRepository.GetStockReturns(clientId); + + var movements = new List(); + + foreach (var g in grns) + foreach (var i in g.Items) + movements.Add(new StockMovementDto + { + Type = "GRN", + ProductId = i.FKProductId, + ProductName = i.Product?.Name ?? string.Empty, + Reference = g.GrnNumber, + QuantityChange = i.Quantity, + Amount = i.LineTotal, + Date = g.ReceivedAt + }); + + foreach (var s in sales) + foreach (var i in s.Items) + movements.Add(new StockMovementDto + { + Type = "Sale", + ProductId = i.FKProductId, + ProductName = i.Product?.Name ?? string.Empty, + Reference = s.SaleNumber, + QuantityChange = -i.Quantity, + Amount = i.LineTotal, + Date = s.SoldAt + }); + + foreach (var r in returns) + movements.Add(new StockMovementDto + { + Type = "Return", + ProductId = r.FKProductId, + ProductName = r.Product?.Name ?? string.Empty, + Reference = r.Sale?.SaleNumber ?? r.Grn?.GrnNumber ?? r.Category, + Category = r.Category, + // CustomerReturn is the one category that adds back to stock -- see + // StockReturn's doc comment. + QuantityChange = r.Category == "CustomerReturn" ? r.Quantity : -r.Quantity, + Amount = r.Amount, + Date = r.ReturnedAt + }); + + return movements.OrderByDescending(m => m.Date).ToList(); + } + /// Staff optionally picking an existing registered member to attach to a sale /// (search-and-pick, not free-typed) -- purely for identification, mirrors /// BookingService.ResolveOptionalCustomerLinkAsync exactly.