Files
Core_Be/Services/InventoryService.cs
T
HarithaRandunu 2eb88b058e Add inventory management features including Sales, Stock Returns, and Suppliers
- Implement Sale and SaleItem models for tracking sales transactions.
- Create StockReturn model to manage stock adjustments for various reasons.
- Introduce Supplier model to represent suppliers of products.
- Develop InventoryRepository for CRUD operations on products, suppliers, sales, and stock returns.
- Implement InventoryService to handle business logic for inventory management.
- Update TenantDbContext to include new models and relationships.
- Enhance ProfitLossRepository to account for sales, purchases, and stock returns in profit and loss calculations.
- Modify Program.cs to register new inventory-related services.
2026-10-07 15:43:20 +05:30

674 lines
29 KiB
C#

using Fitness_core_BE.DTOs.Inventory;
using Fitness_core_BE.Exceptions;
using Fitness_core_BE.Helpers;
using Fitness_core_BE.Infra.UoW;
using Fitness_core_BE.Interfaces;
using Fitness_core_BE.Interfaces.Services;
using Fitness_core_BE.Models;
using System.Text.Json;
namespace Fitness_core_BE.Services
{
public class InventoryService : IInventoryService
{
private static readonly string[] ValidReturnCategories =
["Expired", "Damaged", "Lost", "SupplierReturn", "CustomerReturn", "Other"];
private readonly IInventoryRepository _inventoryRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IUnitOfWork _uow;
private readonly UserRoleHelper _userRoleHelper;
public InventoryService(
IInventoryRepository inventoryRepository,
ICustomerRepository customerRepository,
IUnitOfWork uow,
UserRoleHelper userRoleHelper)
{
_inventoryRepository = inventoryRepository;
_customerRepository = customerRepository;
_uow = uow;
_userRoleHelper = userRoleHelper;
}
// --- Products ---
public async Task<object> GetProducts(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
bool includeInactive = data.ContainsKey("includeInactive") && data["includeInactive"].GetBoolean();
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var products = await _inventoryRepository.GetProducts(clientId, includeInactive);
return (await Task.WhenAll(products.Select(MapToDtoAsync))).ToList();
}
public async Task<object> GetProductById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var productId = Guid.Parse(data["productId"].GetString()!);
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var product = await _inventoryRepository.GetProductById(productId);
if (product == null || product.FKClientId != clientId)
throw new KeyNotFoundException("Product not found");
var grnItems = await _inventoryRepository.GetGrnItemsForProduct(productId);
var saleItems = await _inventoryRepository.GetSaleItemsForProduct(productId);
var returns = await _inventoryRepository.GetStockReturnsForProduct(productId);
var movements = new List<ProductMovementDto>();
movements.AddRange(grnItems.Select(i => new ProductMovementDto
{
Type = "GRN",
Reference = i.Grn?.GrnNumber ?? string.Empty,
QuantityChange = i.Quantity,
Date = i.Grn?.ReceivedAt ?? DateTime.UtcNow
}));
movements.AddRange(saleItems.Select(i => new ProductMovementDto
{
Type = "Sale",
Reference = i.Sale?.SaleNumber ?? string.Empty,
QuantityChange = -i.Quantity,
Date = i.Sale?.SoldAt ?? DateTime.UtcNow
}));
movements.AddRange(returns.Select(r => new ProductMovementDto
{
Type = "Return",
Reference = 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,
Date = r.ReturnedAt
}));
movements = movements.OrderByDescending(m => m.Date).ToList();
var dto = await MapToDtoAsync(product);
return new ProductDetailDto
{
ProductId = dto.ProductId,
Name = dto.Name,
Sku = dto.Sku,
ImageUrl = dto.ImageUrl,
UnitOfMeasure = dto.UnitOfMeasure,
CostPrice = dto.CostPrice,
SellingPrice = dto.SellingPrice,
QuantityOnHand = dto.QuantityOnHand,
IsActive = dto.IsActive,
CreatedAt = dto.CreatedAt,
Movements = movements
};
}
public async Task<object> CreateProduct(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("name")) throw new KeyNotFoundException("name is required");
var sellingPrice = data.ContainsKey("sellingPrice") ? data["sellingPrice"].GetDecimal() : 0m;
if (sellingPrice < 0) throw new ArgumentException("sellingPrice cannot be negative");
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var product = new Product
{
FKClientId = clientId,
Name = data["name"].GetString() ?? "",
UnitOfMeasure = data.ContainsKey("unitOfMeasure") ? data["unitOfMeasure"].GetString() ?? "pcs" : "pcs",
SellingPrice = sellingPrice
};
var sequence = await _inventoryRepository.GetNextProductSequence(clientId);
product.Sku = $"PRD-{sequence:D4}";
var result = await _inventoryRepository.CreateProduct(product);
await _uow.CommitAsync();
return await MapToDtoAsync(result);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UpdateProduct(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var productId = Guid.Parse(data["productId"].GetString()!);
var existing = await _inventoryRepository.GetProductById(productId)
?? throw new KeyNotFoundException("Product not found");
// Sku is system-generated at creation and never editable here.
if (data.ContainsKey("name")) existing.Name = data["name"].GetString() ?? existing.Name;
if (data.ContainsKey("unitOfMeasure")) existing.UnitOfMeasure = data["unitOfMeasure"].GetString() ?? existing.UnitOfMeasure;
if (data.ContainsKey("sellingPrice"))
{
var price = data["sellingPrice"].GetDecimal();
if (price < 0) throw new ArgumentException("sellingPrice cannot be negative");
existing.SellingPrice = price;
}
if (data.ContainsKey("isActive")) existing.IsActive = data["isActive"].GetBoolean();
var result = await _inventoryRepository.UpdateProduct(existing);
await _uow.CommitAsync();
return await MapToDtoAsync(result);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> UploadProductImage(Guid productId, IFormFile image, HttpContext httpContext)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var product = await _inventoryRepository.GetProductById(productId);
if (product == null || product.FKClientId != clientId)
throw new KeyNotFoundException("Product not found");
if (image == null || image.Length == 0)
throw new BadRequestException("No image file provided.");
string extension = Path.GetExtension(image.FileName).ToLowerInvariant();
string[] allowed = { ".jpg", ".jpeg", ".png", ".webp" };
if (!allowed.Contains(extension))
throw new BadRequestException("Image must be a jpg, png or webp file.");
if (image.Length > 5 * 1024 * 1024)
throw new BadRequestException("Image too large (max 5MB).");
string uploadsRoot = Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "Uploads", "product-images", clientId.ToString()));
if (!Directory.Exists(uploadsRoot))
Directory.CreateDirectory(uploadsRoot);
string filename = $"{productId}_{DateTime.UtcNow:yyyyMMdd_HHmmss}{extension}";
string filePath = Path.Combine(uploadsRoot, filename);
using (var stream = new FileStream(filePath, FileMode.Create))
{
await image.CopyToAsync(stream);
}
await _uow.BeginAsync();
try
{
product.ImageId = $"/Uploads/product-images/{clientId}/{Uri.EscapeDataString(filename)}";
await _uow.CommitAsync();
return await MapToDtoAsync(product);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
// --- Suppliers ---
public async Task<object> GetSuppliers(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
bool includeInactive = data.ContainsKey("includeInactive") && data["includeInactive"].GetBoolean();
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var suppliers = await _inventoryRepository.GetSuppliers(clientId, includeInactive);
return suppliers.Select(MapToDto).ToList();
}
public async Task<object> CreateSupplier(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("name")) throw new KeyNotFoundException("name is required");
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var supplier = new Supplier
{
FKClientId = clientId,
Name = data["name"].GetString() ?? "",
Phone = data.ContainsKey("phone") && data["phone"].ValueKind != JsonValueKind.Null ? data["phone"].GetString() : null,
Email = data.ContainsKey("email") && data["email"].ValueKind != JsonValueKind.Null ? data["email"].GetString() : null,
Address = data.ContainsKey("address") && data["address"].ValueKind != JsonValueKind.Null ? data["address"].GetString() : null
};
var result = await _inventoryRepository.CreateSupplier(supplier);
await _uow.CommitAsync();
return MapToDto(result);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
// --- GRN ---
public async Task<object> CreateGrn(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
// Optional -- some GRNs don't have a supplier on file.
Supplier? supplier = null;
Guid? supplierId = data.ContainsKey("supplierId") && data["supplierId"].ValueKind != JsonValueKind.Null
? Guid.Parse(data["supplierId"].GetString()!)
: null;
if (supplierId.HasValue)
{
supplier = await _inventoryRepository.GetSupplierById(supplierId.Value);
if (supplier == null || supplier.FKClientId != clientId)
throw new KeyNotFoundException("Supplier not found");
}
if (!data.ContainsKey("items") || data["items"].ValueKind != JsonValueKind.Array)
throw new ArgumentException("At least one item is required");
var receivedAt = data.ContainsKey("receivedAt") && data["receivedAt"].ValueKind != JsonValueKind.Null
? DateTime.Parse(data["receivedAt"].GetString()!).ToUniversalTime()
: DateTime.UtcNow;
var grn = new GoodsReceivedNote
{
FKClientId = clientId,
FKSupplierId = supplierId,
ReceivedAt = receivedAt,
Notes = data.ContainsKey("notes") && data["notes"].ValueKind != JsonValueKind.Null ? data["notes"].GetString() : null
};
decimal total = 0m;
var itemsArray = data["items"].EnumerateArray().ToList();
if (itemsArray.Count == 0) throw new ArgumentException("At least one item is required");
foreach (var itemEl in itemsArray)
{
var productId = Guid.Parse(itemEl.GetProperty("productId").GetString()!);
var quantity = itemEl.GetProperty("quantity").GetDecimal();
var unitCost = itemEl.GetProperty("unitCost").GetDecimal();
if (quantity <= 0) throw new ArgumentException("Quantity must be greater than 0");
if (unitCost < 0) throw new ArgumentException("UnitCost cannot be negative");
var product = await _inventoryRepository.GetProductById(productId);
if (product == null || product.FKClientId != clientId)
throw new KeyNotFoundException("Product not found");
var lineTotal = quantity * unitCost;
grn.Items.Add(new GoodsReceivedNoteItem
{
FKProductId = productId,
Quantity = quantity,
UnitCost = unitCost,
LineTotal = lineTotal
});
product.QuantityOnHand += quantity;
product.CostPrice = unitCost;
total += lineTotal;
}
grn.TotalAmount = total;
var sequence = await _inventoryRepository.GetNextGrnSequence(clientId);
grn.GrnNumber = $"GRN-{sequence:D4}";
var result = await _inventoryRepository.CreateGrn(grn);
await _uow.CommitAsync();
result.Supplier = supplier;
return MapToDto(result);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetGrns(object payload, HttpContext httpContext)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var grns = await _inventoryRepository.GetGrns(clientId);
return grns.Select(MapToDto).ToList();
}
public async Task<object> GetGrnById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var grnId = Guid.Parse(data["grnId"].GetString()!);
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var grn = await _inventoryRepository.GetGrnById(grnId);
if (grn == null || grn.FKClientId != clientId)
throw new KeyNotFoundException("GRN not found");
return MapToDto(grn);
}
// --- Sales ---
public async Task<object> CreateSale(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var paymentMethod = data.ContainsKey("paymentMethod") && data["paymentMethod"].ValueKind != JsonValueKind.Null
? data["paymentMethod"].GetString() ?? "Cash" : "Cash";
var customerId = await ResolveOptionalCustomerLinkAsync(data);
if (!data.ContainsKey("items") || data["items"].ValueKind != JsonValueKind.Array)
throw new ArgumentException("At least one item is required");
var soldAt = data.ContainsKey("soldAt") && data["soldAt"].ValueKind != JsonValueKind.Null
? DateTime.Parse(data["soldAt"].GetString()!).ToUniversalTime()
: DateTime.UtcNow;
var sale = new Sale
{
FKClientId = clientId,
SoldAt = soldAt,
PaymentMethod = paymentMethod,
FKCustomerId = customerId,
CustomerName = data.ContainsKey("customerName") && data["customerName"].ValueKind != JsonValueKind.Null
? data["customerName"].GetString() : null
};
decimal total = 0m;
var itemsArray = data["items"].EnumerateArray().ToList();
if (itemsArray.Count == 0) throw new ArgumentException("At least one item is required");
// Load + validate every line before mutating stock, so a short line rejects the
// whole sale rather than partially decrementing earlier lines.
var lines = new List<(Product Product, decimal Quantity, decimal UnitPrice)>();
foreach (var itemEl in itemsArray)
{
var productId = Guid.Parse(itemEl.GetProperty("productId").GetString()!);
var quantity = itemEl.GetProperty("quantity").GetDecimal();
var unitPrice = itemEl.GetProperty("unitPrice").GetDecimal();
if (quantity <= 0) throw new ArgumentException("Quantity must be greater than 0");
if (unitPrice < 0) throw new ArgumentException("UnitPrice cannot be negative");
var product = await _inventoryRepository.GetProductById(productId);
if (product == null || product.FKClientId != clientId)
throw new KeyNotFoundException("Product not found");
if (product.QuantityOnHand < quantity)
throw new ArgumentException($"Not enough stock for \"{product.Name}\" (have {product.QuantityOnHand}, need {quantity})");
lines.Add((product, quantity, unitPrice));
}
foreach (var (product, quantity, unitPrice) in lines)
{
var lineTotal = quantity * unitPrice;
sale.Items.Add(new SaleItem
{
FKProductId = product.ProductId,
Quantity = quantity,
UnitPrice = unitPrice,
LineTotal = lineTotal
});
product.QuantityOnHand -= quantity;
total += lineTotal;
}
sale.TotalAmount = total;
var sequence = await _inventoryRepository.GetNextSaleSequence(clientId);
sale.SaleNumber = $"SALE-{sequence:D4}";
var result = await _inventoryRepository.CreateSale(sale);
await _uow.CommitAsync();
return MapToDto(result);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetSales(object payload, HttpContext httpContext)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var sales = await _inventoryRepository.GetSales(clientId);
return sales.Select(MapToDto).ToList();
}
public async Task<object> GetSaleById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var saleId = Guid.Parse(data["saleId"].GetString()!);
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var sale = await _inventoryRepository.GetSaleById(saleId);
if (sale == null || sale.FKClientId != clientId)
throw new KeyNotFoundException("Sale not found");
return MapToDto(sale);
}
// --- Returns ---
public async Task<object> CreateStockReturn(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var productId = Guid.Parse(data["productId"].GetString()!);
var quantity = data["quantity"].GetDecimal();
if (quantity <= 0) throw new ArgumentException("Quantity must be greater than 0");
var category = data["category"].GetString() ?? "Other";
if (!ValidReturnCategories.Contains(category))
throw new ArgumentException($"category must be one of: {string.Join(", ", ValidReturnCategories)}");
var product = await _inventoryRepository.GetProductById(productId);
if (product == null || product.FKClientId != clientId)
throw new KeyNotFoundException("Product not found");
// CustomerReturn is the one category that adds back to stock -- the item
// physically comes back after an earlier sale already removed it, so there's no
// "enough stock on hand" requirement (it may well be at 0). Every other category
// removes from stock we currently hold, so it does require enough on hand.
var addsBackToStock = category == "CustomerReturn";
if (!addsBackToStock && product.QuantityOnHand < quantity)
throw new ArgumentException($"Not enough stock for \"{product.Name}\" (have {product.QuantityOnHand}, need {quantity})");
// Optional traceability -- most returns have neither. Only validated for
// existence/ownership, not cross-checked against the product/quantity.
Guid? saleId = data.ContainsKey("saleId") && data["saleId"].ValueKind != JsonValueKind.Null
? Guid.Parse(data["saleId"].GetString()!) : null;
Sale? sale = null;
if (saleId.HasValue)
{
sale = await _inventoryRepository.GetSaleById(saleId.Value);
if (sale == null || sale.FKClientId != clientId)
throw new KeyNotFoundException("Sale not found");
}
Guid? grnId = data.ContainsKey("grnId") && data["grnId"].ValueKind != JsonValueKind.Null
? Guid.Parse(data["grnId"].GetString()!) : null;
GoodsReceivedNote? grn = null;
if (grnId.HasValue)
{
grn = await _inventoryRepository.GetGrnById(grnId.Value);
if (grn == null || grn.FKClientId != clientId)
throw new KeyNotFoundException("GRN not found");
}
var returnedAt = data.ContainsKey("returnedAt") && data["returnedAt"].ValueKind != JsonValueKind.Null
? DateTime.Parse(data["returnedAt"].GetString()!).ToUniversalTime()
: DateTime.UtcNow;
var stockReturn = new StockReturn
{
FKClientId = clientId,
FKProductId = productId,
Quantity = quantity,
Category = category,
FKSaleId = saleId,
FKGrnId = grnId,
Notes = data.ContainsKey("notes") && data["notes"].ValueKind != JsonValueKind.Null ? data["notes"].GetString() : null,
ReturnedAt = returnedAt,
Amount = quantity * product.CostPrice
};
product.QuantityOnHand += addsBackToStock ? quantity : -quantity;
var result = await _inventoryRepository.CreateStockReturn(stockReturn);
await _uow.CommitAsync();
result.Product = product;
result.Sale = sale;
result.Grn = grn;
return MapToDto(result);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetStockReturns(object payload, HttpContext httpContext)
{
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var returns = await _inventoryRepository.GetStockReturns(clientId);
return returns.Select(MapToDto).ToList();
}
public async Task<object> GetStockReturnById(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var stockReturnId = Guid.Parse(data["stockReturnId"].GetString()!);
var clientId = await _userRoleHelper.GetClientId(DataExtractor.GetUserIdFromContext(httpContext));
var stockReturn = await _inventoryRepository.GetStockReturnById(stockReturnId);
if (stockReturn == null || stockReturn.FKClientId != clientId)
throw new KeyNotFoundException("Return not found");
return MapToDto(stockReturn);
}
/// <summary>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.</summary>
private async Task<Guid?> ResolveOptionalCustomerLinkAsync(Dictionary<string, JsonElement> data)
{
if (!data.ContainsKey("customerId") || data["customerId"].ValueKind == JsonValueKind.Null)
return null;
var customerId = Guid.Parse(data["customerId"].GetString()!);
_ = await _customerRepository.GetTenantCustomerLinkById(customerId)
?? throw new KeyNotFoundException("Customer not found");
return customerId;
}
// ImageId is a relative /Uploads path (same convention as ClientBusiness.BusinessLogoId)
// with no static-file serving configured for it, so it's inlined as a base64 data URL.
private static async Task<ProductDto> MapToDtoAsync(Product p) => new()
{
ProductId = p.ProductId,
Name = p.Name,
Sku = p.Sku,
ImageUrl = await BusinessLogoHelper.ReadAsDataUrlAsync(p.ImageId),
UnitOfMeasure = p.UnitOfMeasure,
CostPrice = p.CostPrice,
SellingPrice = p.SellingPrice,
QuantityOnHand = p.QuantityOnHand,
IsActive = p.IsActive,
CreatedAt = p.CreatedAt
};
private static SupplierDto MapToDto(Supplier s) => new()
{
SupplierId = s.SupplierId,
Name = s.Name,
Phone = s.Phone,
Email = s.Email,
Address = s.Address,
IsActive = s.IsActive
};
private static GoodsReceivedNoteDto MapToDto(GoodsReceivedNote g) => new()
{
GrnId = g.GrnId,
GrnNumber = g.GrnNumber,
SupplierId = g.FKSupplierId,
SupplierName = g.Supplier?.Name,
ReceivedAt = g.ReceivedAt,
Notes = g.Notes,
TotalAmount = g.TotalAmount,
Items = g.Items.Select(i => new GoodsReceivedNoteItemDto
{
ProductId = i.FKProductId,
ProductName = i.Product?.Name ?? string.Empty,
Quantity = i.Quantity,
UnitCost = i.UnitCost,
LineTotal = i.LineTotal
}).ToList()
};
private static SaleDto MapToDto(Sale s) => new()
{
SaleId = s.SaleId,
SaleNumber = s.SaleNumber,
SoldAt = s.SoldAt,
PaymentMethod = s.PaymentMethod,
CustomerId = s.FKCustomerId,
CustomerName = s.CustomerName,
TotalAmount = s.TotalAmount,
Items = s.Items.Select(i => new SaleItemDto
{
ProductId = i.FKProductId,
ProductName = i.Product?.Name ?? string.Empty,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice,
LineTotal = i.LineTotal
}).ToList()
};
private static StockReturnDto MapToDto(StockReturn r) => new()
{
StockReturnId = r.StockReturnId,
ProductId = r.FKProductId,
ProductName = r.Product?.Name ?? string.Empty,
Quantity = r.Quantity,
Category = r.Category,
SaleId = r.FKSaleId,
SaleNumber = r.Sale?.SaleNumber,
GrnId = r.FKGrnId,
GrnNumber = r.Grn?.GrnNumber,
Notes = r.Notes,
ReturnedAt = r.ReturnedAt,
Amount = r.Amount
};
}
}