From 4d217401248d827faeea9902d7973221577e9773 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Sat, 30 May 2026 16:07:23 +0200 Subject: [PATCH] Added category seeder --- .../CategorySeederService.cs | 133 ++++++++++++++++++ .../Program.cs | 1 + .../appsettings.json | 1 + .../AuthorBooks/BooksService.cs | 2 +- 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs diff --git a/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs new file mode 100644 index 0000000..5856761 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/CategorySeederService.cs @@ -0,0 +1,133 @@ +using LiteCharms.Features.MidrandBooks.Categories; +using LiteCharms.Features.MidrandBooks.Products; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!await features.IsEnabledAsync("CategorySeederService")) return; + + logger.LogInformation("Category and Product-Tag Mapping Seeding started (15-char limit applied)"); + + // Initialize Bogus to ensure repeatable distribution matrix pathing + var faker = new Faker(); + Randomizer.Seed = new Random(101); + + // 1. Curate Broad Book Categories (IsMain = true, Max 20, Max 15 chars) + var broadMainCategories = new[] + { + "Fiction", "Non-Fiction", "Youth & Kids", "Academic", + "Biographies", "Business", "Sci-Fi & Fantasy", + "Thrillers", "Self-Help", "History", + "Spirituality", "Arts & Photo", "Technology", + "Cookbooks", "Travel & Maps", "Poetry & Drama", "Graphic Novels" + }; + + // 2. Curate Niche Subcategories/Tags (IsMain = false, Max 15 chars) + var specializedSubCategories = new[] + { + "Cyberpunk", "Space Opera", "Historical Fix", "Cozy Mystery", "True Crime", + "Agile Project", "Software Eng", "AI & ML", "Cloud Comput", + "SA History", "African Lit", "Apartheid Era", "Mandela Legacy", + "Finance", "Investments", "Startup", "Leadership", + "CBT Therapy", "Mindfulness", "Yoga & Health", + "Baking Basics", "African Food", "Vegan Recipes", + "Ancient World", "WWII History", "Geopolitics", + "Writing Guides", "Criticism", "Classic Poetry", + "Early Learning", "Teen Romance", "Survival", + "Urban Fantasy", "Dark Fantasy", "Psych Thriller", "Hard Sci-Fi", + "Data Science", "DevOps", "Cybersecurity", + "Economics", "Real Estate", "Governance", + "Essays", "Memoirs", "Art History", + "Architecture", "Photography", "Travel Writing", + "Gaming Culture", "Philosophy", "Ethics", + "DIY Home", "SA Gardening", "Parenting" + }; + + // 3. Seed Main Categories into the System + logger.LogInformation("Seeding broad main categories..."); + foreach (var mainCat in broadMainCategories) + { + if (stoppingToken.IsCancellationRequested) return; + + // Defensive truncation fallback just in case strings get modified later + string safeName = mainCat.Length > 15 ? mainCat.Substring(0, 15) : mainCat; + + var result = await categoryService.CreateCategoryAsync(safeName, isMain: true, stoppingToken); + if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) + { + logger.LogWarning("Notice while adding main category '{Name}': {Msg}", safeName, result.Errors[0].Message); + } + } + + // 4. Seed Subcategories into the System + logger.LogInformation("Seeding boundless specialized niche tags..."); + foreach (var subCat in specializedSubCategories) + { + if (stoppingToken.IsCancellationRequested) return; + + string safeName = subCat.Length > 15 ? subCat.Substring(0, 15) : subCat; + + var result = await categoryService.CreateCategoryAsync(safeName, isMain: false, stoppingToken); + if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase)) + { + logger.LogWarning("Notice while adding subcategory '{Name}': {Msg}", safeName, result.Errors[0].Message); + } + } + + // 5. Query back all enabled categories to extract active IDs for junction mapping + var fetchMainResult = await categoryService.GetCategoriesAsync(isMain: true, stoppingToken); + var fetchSubResult = await categoryService.GetCategoriesAsync(isMain: false, stoppingToken); + + if (fetchMainResult.IsFailed || fetchSubResult.IsFailed) + { + logger.LogError("Aborting junction seeding: Could not retrieve categories from data store."); + return; + } + + var mainCategoryIds = fetchMainResult.Value.Select(c => c.Id).ToArray(); + var subCategoryIds = fetchSubResult.Value.Select(c => c.Id).ToArray(); + + // 6. Map Categories to your Product Collection (Product IDs 0 - 21) + logger.LogInformation("Beginning Product-Category mapping assignments for Product IDs 0 through 21..."); + + for (long productId = 0; productId <= 21; productId++) + { + if (stoppingToken.IsCancellationRequested) break; + + // Every book belongs to 1 or 2 main categories + int mainCategoriesToAssign = faker.Random.Number(1, 2); + var chosenMainIds = faker.PickRandom(mainCategoryIds, mainCategoriesToAssign).Distinct(); + + foreach (var mainId in chosenMainIds) + { + var linkResult = await productService.AddProductCategoryAsync(productId, mainId, stoppingToken); + if (linkResult.IsFailed) + { + if (!linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Junction note for Product {PId} and Main Category {CId}: {Msg}", productId, mainId, linkResult.Errors[0].Message); + } + } + } + + // Every book gets 1 to 4 granular subgenre tags + int subCategoriesToAssign = faker.Random.Number(1, 4); + var chosenSubIds = faker.PickRandom(subCategoryIds, subCategoriesToAssign).Distinct(); + + foreach (var subId in chosenSubIds) + { + var linkResult = await productService.AddProductCategoryAsync(productId, subId, stoppingToken); + if (linkResult.IsFailed && !linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug("Junction note for Product {PId} and Sub Category {CId}: {Msg}", productId, subId, linkResult.Errors[0].Message); + } + } + } + + logger.LogInformation("Category and Product-Tag Mapping Seeding completed successfully."); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/Program.cs b/LiteCharms.Features.MidrandBooks.Seed/Program.cs index 01b4fbf..10d3172 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/Program.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/Program.cs @@ -16,6 +16,7 @@ builder.Services .AddLogging() .AddShopServices() .AddHostedService() + .AddHostedService() .AddHostedService() .AddMidrandShopDatabase(builder.Configuration); diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json index b710f54..b394b55 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -1,5 +1,6 @@ { "FeatureManagement": { + "CategorySeederService": true, "CustomerSeederService": false, "ProductsSeederService": false }, diff --git a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs index 4375d8e..545fe87 100644 --- a/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs +++ b/LiteCharms.Features.MidrandBooks/AuthorBooks/BooksService.cs @@ -95,7 +95,7 @@ public sealed class BooksService(IDbContextFactory contex .AsNoTracking() .Include(b => b.Author) .Include(b => b.Product) - .ThenInclude(b => b.Prices) + .ThenInclude(b => b!.Prices) .OrderByDescending(b => b.CreatedAt) .Where(b => b.AuthorId == authorId) .ToListAsync(cancellationToken);