Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7a798b5e9 | |||
| 494b806744 | |||
| 41b6b71b31 | |||
| 0702caa42d | |||
| ee6beef603 | |||
| 4f6dbfcd37 | |||
| 1c3f3eaf0d | |||
| 91ede2d568 | |||
| 2e77666d9e | |||
| 4d21740124 |
@@ -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()
|
||||
.AddShopServices()
|
||||
.AddHostedService<ProductsSeederService>()
|
||||
.AddHostedService<CategorySeederService>()
|
||||
.AddHostedService<CustomerSeederService>()
|
||||
.AddMidrandShopDatabase(builder.Configuration);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"FeatureManagement": {
|
||||
"CategorySeederService": true,
|
||||
"CustomerSeederService": false,
|
||||
"ProductsSeederService": false
|
||||
},
|
||||
|
||||
@@ -95,7 +95,7 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> 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);
|
||||
|
||||
@@ -8,6 +8,29 @@ namespace LiteCharms.Features.MidrandBooks.Authors;
|
||||
|
||||
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)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -304,7 +304,8 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
|
||||
{
|
||||
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
|
||||
.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
|
||||
|
||||
return product is null
|
||||
? Result.Fail<Product>(new Error($"Product with ID {productId} not found."))
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class SearchState
|
||||
{
|
||||
public string Query { get; private set; } = string.Empty;
|
||||
|
||||
public event Action? OnSearchSubmitted;
|
||||
|
||||
public void UpdateQuery(string newQuery) => Query = newQuery;
|
||||
|
||||
public void SubmitSearch() => OnSearchSubmitted?.Invoke();
|
||||
}
|
||||
Reference in New Issue
Block a user