Compare commits

...

6 Commits

Author SHA1 Message Date
khwezi ee6beef603 Merge pull request 'Redacted Product.Price mapping on filter' (#50) from midrandshop into master
Reviewed-on: #50
2026-05-30 18:49:38 +02:00
Khwezi Mngoma 4f6dbfcd37 Redacted Product.Price mapping on filter
continuous-integration/drone/pr Build is passing
2026-05-30 18:49:15 +02:00
khwezi 1c3f3eaf0d Merge pull request 'Added a way to get the Author by productId' (#49) from midrandshop into master
Reviewed-on: #49
2026-05-30 18:20:21 +02:00
Khwezi Mngoma 91ede2d568 Added a way to get the Author by productId
continuous-integration/drone/pr Build is passing
2026-05-30 18:17:55 +02:00
khwezi 2e77666d9e Merge pull request 'Added category seeder' (#48) from midrandshop into master
Reviewed-on: #48
2026-05-30 16:08:11 +02:00
Khwezi Mngoma 4d21740124 Added category seeder
continuous-integration/drone/pr Build is passing
2026-05-30 16:07:23 +02:00
6 changed files with 162 additions and 2 deletions
@@ -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<CategorySeederService> 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.");
}
}
@@ -16,6 +16,7 @@ builder.Services
.AddLogging() .AddLogging()
.AddShopServices() .AddShopServices()
.AddHostedService<ProductsSeederService>() .AddHostedService<ProductsSeederService>()
.AddHostedService<CategorySeederService>()
.AddHostedService<CustomerSeederService>() .AddHostedService<CustomerSeederService>()
.AddMidrandShopDatabase(builder.Configuration); .AddMidrandShopDatabase(builder.Configuration);
@@ -1,5 +1,6 @@
{ {
"FeatureManagement": { "FeatureManagement": {
"CategorySeederService": true,
"CustomerSeederService": false, "CustomerSeederService": false,
"ProductsSeederService": false "ProductsSeederService": false
}, },
@@ -95,7 +95,7 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
.AsNoTracking() .AsNoTracking()
.Include(b => b.Author) .Include(b => b.Author)
.Include(b => b.Product) .Include(b => b.Product)
.ThenInclude(b => b.Prices) .ThenInclude(b => b!.Prices)
.OrderByDescending(b => b.CreatedAt) .OrderByDescending(b => b.CreatedAt)
.Where(b => b.AuthorId == authorId) .Where(b => b.AuthorId == authorId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -8,6 +8,29 @@ namespace LiteCharms.Features.MidrandBooks.Authors;
public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{ {
public async ValueTask<Result<Author>> GetAuthorByProductIdAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var author = await context.Books
.AsNoTracking()
.Include(i => i.Author)
.Where(b => b.ProductId == productId)
.FirstOrDefaultAsync(cancellationToken);
if (author is null)
return Result.Fail<Author>(new Error($"No author association discovered for Product ID {productId}"));
return Result.Ok(author.Author!.ToModel());
}
catch (Exception ex)
{
return Result.Fail<Author>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default) public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default)
{ {
try try
@@ -304,7 +304,9 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); var product = await context.Products
.Include(i => i.Price)
.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
return product is null return product is null
? Result.Fail<Product>(new Error($"Product with ID {productId} not found.")) ? Result.Fail<Product>(new Error($"Product with ID {productId} not found."))