From 7389cc2c4152351138e29e7d124b2f614dde0d50 Mon Sep 17 00:00:00 2001 From: Mario Medhat <118922155+Mariomedhat899@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:35:04 +0300 Subject: [PATCH 1/2] Added smtp services and interfaces --- IMS.API/Controllers/TransactionsController.cs | 27 ++++++++++++- IMS.API/IMS.API.csproj | 4 +- .../Models/ReportsDtos/InventoryReportDto.cs | 4 +- IMS.API/Program.cs | 23 ++++++++++- IMS.API/appsettings.json | 15 +++++++ IMS.Core/Contracts/IEmailService.cs | 7 ++++ IMS.Core/Entities/Category.cs | 2 +- IMS.Infrastructure/Data/DataSeeder.cs | 39 +++++++++++++++++-- IMS.Infrastructure/Data/RoleSeeder.cs | 37 +++++++++++++++++- .../Data/SeedData/Products.json | 20 +++++----- .../Services/NotificationSettings.cs | 8 ++++ .../Services/SmtpEmailService.cs | 38 ++++++++++++++++++ IMS.Infrastructure/Services/SmtpSettings.cs | 19 +++++++++ 13 files changed, 221 insertions(+), 22 deletions(-) create mode 100644 IMS.Core/Contracts/IEmailService.cs create mode 100644 IMS.Infrastructure/Services/NotificationSettings.cs create mode 100644 IMS.Infrastructure/Services/SmtpEmailService.cs create mode 100644 IMS.Infrastructure/Services/SmtpSettings.cs diff --git a/IMS.API/Controllers/TransactionsController.cs b/IMS.API/Controllers/TransactionsController.cs index e46611a..cf89584 100644 --- a/IMS.API/Controllers/TransactionsController.cs +++ b/IMS.API/Controllers/TransactionsController.cs @@ -1,9 +1,12 @@ using IMS.API.Models.TransactionsDtos; +using IMS.Core.Contracts; using IMS.Core.Entities; using IMS.Infrastructure.Data; +using IMS.Infrastructure.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; using System.Security.Claims; @@ -11,9 +14,14 @@ namespace IMS.API.Controllers { [Route("api/[controller]")] [ApiController] - public class TransactionsController(ApplicationDbContext _context) : ControllerBase + public class TransactionsController(ApplicationDbContext _context + , IEmailService emailService, + IOptions notificationSettings) : ControllerBase { + private readonly IEmailService _emailService = emailService; + private readonly NotificationSettings _notificationSettings = notificationSettings.Value; + [HttpGet] [Authorize(Roles = "Admin,Manager")] public async Task GetTransactions() @@ -63,6 +71,23 @@ public async Task RecordTransaction([FromBody] CreateTransactionD return BadRequest("Insufficient stock for this sale!!"); product.QuantityInStock -= model.Quantity; + if (product.QuantityInStock <= _notificationSettings.LowStockThreshold) + { + var subject = $"Low Stock Alert: {product.Name}"; + var body = $"Product '{product.Name}'(ID: {product.Id}) has low stock. \n" + + $"Current quantity: {product.QuantityInStock}\n" + + $"Threshold: {_notificationSettings.LowStockThreshold}\n" + + $"Please restock this item soon"; + + try + { + await _emailService.SendEmailAsync(_notificationSettings.AdminEmail, subject, body); + } + catch (Exception ex) + { + Console.WriteLine($"Failed to send low stock email: {ex.Message}"); + } + } } else { diff --git a/IMS.API/IMS.API.csproj b/IMS.API/IMS.API.csproj index 5dcf008..f9c9281 100644 --- a/IMS.API/IMS.API.csproj +++ b/IMS.API/IMS.API.csproj @@ -4,10 +4,10 @@ net8.0 enable enable + 3746ec4c-ff25-44f1-8235-94d8fb6bffb6 - + PreserveNewest diff --git a/IMS.API/Models/ReportsDtos/InventoryReportDto.cs b/IMS.API/Models/ReportsDtos/InventoryReportDto.cs index 70d793b..53c4c60 100644 --- a/IMS.API/Models/ReportsDtos/InventoryReportDto.cs +++ b/IMS.API/Models/ReportsDtos/InventoryReportDto.cs @@ -6,9 +6,9 @@ public class InventoryReportDto public int TotalProducts { get; set; } - public ReportSummaryDto Sales { get; set; } + public ReportSummaryDto Sales { get; set; } = new ReportSummaryDto(); - public ReportSummaryDto Purchases { get; set; } + public ReportSummaryDto Purchases { get; set; } = new ReportSummaryDto(); public List TopSellingProducts { get; set; } = new List(); } diff --git a/IMS.API/Program.cs b/IMS.API/Program.cs index 6750a4e..f9ad3bf 100644 --- a/IMS.API/Program.cs +++ b/IMS.API/Program.cs @@ -1,6 +1,8 @@ using IMS.API.Services; +using IMS.Core.Contracts; using IMS.Core.Entities; using IMS.Infrastructure.Data; +using IMS.Infrastructure.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -13,6 +15,7 @@ + var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(); @@ -81,6 +84,12 @@ }); }); + + +builder.Services.Configure(builder.Configuration.GetSection("SmtpSettings")); +builder.Services.AddScoped(); +builder.Services.Configure(builder.Configuration.GetSection("NotificationSettings")); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -96,7 +105,17 @@ app.UseAuthorization(); app.MapControllers(); -await RoleSeeder.SeedRolesAsync(app.Services); -await DataSeeder.SeedDataAsync(app.Services); + + + +using (var seedScope = app.Services.CreateScope()) +{ + + var dbContext = seedScope.ServiceProvider.GetRequiredService(); + await RoleSeeder.SeedRolesAsync(dbContext, seedScope.ServiceProvider); + + await DataSeeder.SeedDataAsync(seedScope.ServiceProvider); +} + app.Run(); diff --git a/IMS.API/appsettings.json b/IMS.API/appsettings.json index e4d4499..e0413ea 100644 --- a/IMS.API/appsettings.json +++ b/IMS.API/appsettings.json @@ -16,6 +16,21 @@ "Audience": "InventoryManagementApi", "SecretKey": "YourSuperSecretKey123456789012345678901234567890", "ExpiryMinutes": 60 + }, + "SmtpSettings": { + "Host": "smtp.gmail.com", + "Port": 587, + "Username": "", + "Password": "", + "EnableSsl": true, + "FromEmail": "", + "FromName": "IMS Inventory System" + }, + "NotificationSettings": { + "AdminEmail": "mariomedhat899@gmail.com", + "LowStockThreshold" : 10 } } + + diff --git a/IMS.Core/Contracts/IEmailService.cs b/IMS.Core/Contracts/IEmailService.cs new file mode 100644 index 0000000..0c558f5 --- /dev/null +++ b/IMS.Core/Contracts/IEmailService.cs @@ -0,0 +1,7 @@ +namespace IMS.Core.Contracts +{ + public interface IEmailService + { + public Task SendEmailAsync(string email, string subject, string body); + } +} diff --git a/IMS.Core/Entities/Category.cs b/IMS.Core/Entities/Category.cs index 5dc2e44..1c555c4 100644 --- a/IMS.Core/Entities/Category.cs +++ b/IMS.Core/Entities/Category.cs @@ -3,7 +3,7 @@ public class Category : BaseEntity { - public string Name { get; set; } + public string Name { get; set; } = string.Empty; public string? Description { get; set; } public ICollection? Products { get; set; } diff --git a/IMS.Infrastructure/Data/DataSeeder.cs b/IMS.Infrastructure/Data/DataSeeder.cs index f417999..9df9ff8 100644 --- a/IMS.Infrastructure/Data/DataSeeder.cs +++ b/IMS.Infrastructure/Data/DataSeeder.cs @@ -37,22 +37,55 @@ public static async Task SeedDataAsync(IServiceProvider service) await context.SaveChangesAsync(); - + var categoryMap = await context.categories + .ToDictionaryAsync(c => c.Name!, c => c.Id); var productPath = Path.Combine(AppContext.BaseDirectory, "Data", "SeedData", "Products.json"); var productsData = await File.ReadAllTextAsync(productPath); + var productOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; - var products = JsonSerializer.Deserialize>(productsData); + var products = JsonSerializer.Deserialize>(productsData, productOptions); if (products is null) return; + foreach (var product in products) + { + if (!categoryMap.TryGetValue(product.CategoryName, out int categoryID)) + continue; + + context.Products.Add(new Product + { + Name = product.Name, + Description = product.Description, + Price = product.Price, + QuantityInStock = product.QuantityInStock, + Supplier = product.Supplier, + CategoryId = categoryID, + }); + + } + ; - await context.Products.AddRangeAsync(products); await context.SaveChangesAsync(); + } + private class ProductSeedDto + { + + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public decimal Price { get; set; } + public int QuantityInStock { get; set; } + public string Supplier { get; set; } = string.Empty; + public string CategoryName { get; set; } = string.Empty; + } } } + diff --git a/IMS.Infrastructure/Data/RoleSeeder.cs b/IMS.Infrastructure/Data/RoleSeeder.cs index b2a9796..dcefc15 100644 --- a/IMS.Infrastructure/Data/RoleSeeder.cs +++ b/IMS.Infrastructure/Data/RoleSeeder.cs @@ -1,13 +1,16 @@ using IMS.Core.Entities; using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace IMS.Infrastructure.Data { public class RoleSeeder { - public static async Task SeedRolesAsync(IServiceProvider serviceProvider) + public static async Task SeedRolesAsync(ApplicationDbContext _context, IServiceProvider serviceProvider) { + if (_context.Database.GetPendingMigrationsAsync().GetAwaiter().GetResult().Any()) + await _context.Database.MigrateAsync(); using var scope = serviceProvider.CreateScope(); var roleManager = scope.ServiceProvider.GetRequiredService>(); @@ -44,6 +47,38 @@ public static async Task SeedRolesAsync(IServiceProvider serviceProvider) await userManager.AddToRoleAsync(adminUser, "Admin"); } } + var managerEmail = "Manager@test.com"; + var managerUser = await userManager.FindByEmailAsync(managerEmail); + if (managerUser is null) + { + managerUser = new ApplicationUser + { + UserName = "manager", + Email = managerEmail, + EmailConfirmed = true + }; + + var managerResult = await userManager.CreateAsync(managerUser, "Manager@123"); + if (managerResult.Succeeded) + { + await userManager.AddToRoleAsync(managerUser, "Manager"); + } + } + + + var staffEmail = "staff@test.com"; + var staffUser = await userManager.FindByEmailAsync(staffEmail); + if (staffUser is null) + { + staffUser = new ApplicationUser + { + UserName = "staff", + Email = staffEmail, + EmailConfirmed = true + }; + var staffResult = await userManager.CreateAsync(staffUser, "Staff@123"); + if (staffResult.Succeeded) await userManager.AddToRoleAsync(staffUser, "Staff"); + } diff --git a/IMS.Infrastructure/Data/SeedData/Products.json b/IMS.Infrastructure/Data/SeedData/Products.json index 1976d5b..b3e3522 100644 --- a/IMS.Infrastructure/Data/SeedData/Products.json +++ b/IMS.Infrastructure/Data/SeedData/Products.json @@ -5,7 +5,7 @@ "Price": 999.99, "QuantityInStock": 15, "Supplier": "TechCorp", - "CategoryId": 3 + "CategoryName": "Electronics" }, { "Name": "Smartphone", @@ -13,7 +13,7 @@ "Price": 699.99, "QuantityInStock": 30, "Supplier": "TechCorp", - "CategoryId": 4 + "CategoryName": "Electronics" }, { "Name": "Headphones", @@ -21,7 +21,7 @@ "Price": 149.99, "QuantityInStock": 50, "Supplier": "AudioMax", - "CategoryId": 4 + "CategoryName": "Electronics" }, { "Name": "T-Shirt", @@ -29,7 +29,7 @@ "Price": 19.99, "QuantityInStock": 100, "Supplier": "FashionCo", - "CategoryId": 5 + "CategoryName": "Clothing" }, { "Name": "Jeans", @@ -37,7 +37,7 @@ "Price": 49.99, "QuantityInStock": 60, "Supplier": "FashionCo", - "CategoryId": 5 + "CategoryName": "Clothing" }, { "Name": "Coffee Beans", @@ -45,7 +45,7 @@ "Price": 12.99, "QuantityInStock": 200, "Supplier": "BrewMasters", - "CategoryId": 5 + "CategoryName": "Food & Beverages" }, { "Name": "Energy Drink", @@ -53,7 +53,7 @@ "Price": 3.99, "QuantityInStock": 150, "Supplier": "BuzzDrinks", - "CategoryId": 6 + "CategoryName": "Food & Beverages" }, { "Name": "Notebook", @@ -61,7 +61,7 @@ "Price": 5.99, "QuantityInStock": 300, "Supplier": "PaperMill", - "CategoryId": 6 + "CategoryName": "Office Supplies" }, { "Name": "Pen Set", @@ -69,7 +69,7 @@ "Price": 8.99, "QuantityInStock": 250, "Supplier": "PaperMill", - "CategoryId": 6 + "CategoryName": "Office Supplies" }, { "Name": "Hand Sanitizer", @@ -77,6 +77,6 @@ "Price": 4.99, "QuantityInStock": 180, "Supplier": "CleanCare", - "CategoryId": 7 + "CategoryName": "Health & Beauty" } ] \ No newline at end of file diff --git a/IMS.Infrastructure/Services/NotificationSettings.cs b/IMS.Infrastructure/Services/NotificationSettings.cs new file mode 100644 index 0000000..bc836c6 --- /dev/null +++ b/IMS.Infrastructure/Services/NotificationSettings.cs @@ -0,0 +1,8 @@ +namespace IMS.Infrastructure.Services +{ + public class NotificationSettings + { + public string AdminEmail { get; set; } = string.Empty; + public int LowStockThreshold { get; set; } = 10; + } +} diff --git a/IMS.Infrastructure/Services/SmtpEmailService.cs b/IMS.Infrastructure/Services/SmtpEmailService.cs new file mode 100644 index 0000000..afcf996 --- /dev/null +++ b/IMS.Infrastructure/Services/SmtpEmailService.cs @@ -0,0 +1,38 @@ +using IMS.Core.Contracts; +using Microsoft.Extensions.Options; +using System.Net; +using System.Net.Mail; + +namespace IMS.Infrastructure.Services +{ + public class SmtpEmailService : IEmailService + { + private readonly SmtpSettings _smtpSettings; + + public SmtpEmailService(IOptions smtpSettings) + { + _smtpSettings = smtpSettings.Value; + + } + + public async Task SendEmailAsync(string email, string subject, string body) + { + var mailMessage = new MailMessage() + { + From = new MailAddress(_smtpSettings.FromEmail, _smtpSettings.FromName), + Subject = subject, + Body = body, + IsBodyHtml = false + }; + mailMessage.To.Add(new MailAddress(email)); + var smtpClient = new SmtpClient(_smtpSettings.Host, _smtpSettings.Port) + { + Credentials = new NetworkCredential(_smtpSettings.Username, + _smtpSettings.Password), + EnableSsl = _smtpSettings.EnableSsl + }; + + await smtpClient.SendMailAsync(mailMessage); + } + } +} diff --git a/IMS.Infrastructure/Services/SmtpSettings.cs b/IMS.Infrastructure/Services/SmtpSettings.cs new file mode 100644 index 0000000..fbd0c6b --- /dev/null +++ b/IMS.Infrastructure/Services/SmtpSettings.cs @@ -0,0 +1,19 @@ +namespace IMS.Infrastructure.Services +{ + public class SmtpSettings + { + public string Host { get; set; } = string.Empty; + + public int Port { get; set; } + + public string Username { get; set; } = string.Empty; + + public string Password { get; set; } = string.Empty; + + public bool EnableSsl { get; set; } + + public string FromEmail { get; set; } = string.Empty; + + public string FromName { get; set; } = string.Empty; + } +} From 1bba40afbef3f6c7a2be95fa718634adc17695fa Mon Sep 17 00:00:00 2001 From: Mario Medhat <118922155+Mariomedhat899@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:12:17 +0300 Subject: [PATCH 2/2] Fixing csv service --- IMS.API/Services/CsvService.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/IMS.API/Services/CsvService.cs b/IMS.API/Services/CsvService.cs index 15b4611..77240ba 100644 --- a/IMS.API/Services/CsvService.cs +++ b/IMS.API/Services/CsvService.cs @@ -11,14 +11,32 @@ public string ExportProductsToCsv(IEnumerable products) { using var writer = new StringWriter(); using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture); - - csv.WriteHeader(); + csv.WriteField("Id"); + csv.WriteField("Name"); + csv.WriteField("Descrption"); + csv.WriteField("Price"); + csv.WriteField("QuantityInStock"); + csv.WriteField("Supplier"); + csv.WriteField("CategoryId"); + csv.WriteField("CreatedAt"); + csv.WriteField("LastUpdatedAt"); csv.NextRecord(); + + foreach (var product in products) { - csv.WriteRecord(product); + csv.WriteField(product.Id); + csv.WriteField(product.Name); + csv.WriteField(product.Description); + csv.WriteField(product.Price); + csv.WriteField(product.QuantityInStock); + csv.WriteField(product.Supplier); + csv.WriteField(product.CategoryId); + csv.WriteField(product.CreatedAt); + csv.WriteField(product.LastUpdatedAt); csv.NextRecord(); + } ;