From 60fcc70e9831ab5b41a2dd7f151a36fc37bda7d7 Mon Sep 17 00:00:00 2001 From: Khwezi Mngoma Date: Fri, 29 May 2026 18:56:08 +0200 Subject: [PATCH] Implemented Product Data Seeder --- .../Configuration/CdnSettings.cs | 8 + ...teCharms.Features.MidrandBooks.Seed.csproj | 158 +++++++++++++++ .../ProductsSeederService.cs | 190 ++++++++++++++++++ .../Program.cs | 21 ++ .../appsettings.json | 28 +++ LiteCharms.Features/Enums.cs | 3 +- LiteCharmsShared.slnx | 1 + 7 files changed, 408 insertions(+), 1 deletion(-) create mode 100644 LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs create mode 100644 LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj create mode 100644 LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs create mode 100644 LiteCharms.Features.MidrandBooks.Seed/Program.cs create mode 100644 LiteCharms.Features.MidrandBooks.Seed/appsettings.json diff --git a/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs new file mode 100644 index 0000000..af16232 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/Configuration/CdnSettings.cs @@ -0,0 +1,8 @@ +namespace LiteCharms.Features.MidrandBooks.Seed.Configuration; + +public class CdnSettings +{ + public string? BaseCdn { get; set; } + + public string[]? BookCovers { get; set; } +} diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj new file mode 100644 index 0000000..14e2ffd --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -0,0 +1,158 @@ + + + + Exe + net10.0 + enable + enable + 5c3bc894-8654-4691-99e8-f90d3414843f + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs new file mode 100644 index 0000000..a506bf3 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs @@ -0,0 +1,190 @@ +using LiteCharms.Features.MidrandBooks.AuthorBooks; +using LiteCharms.Features.MidrandBooks.Authors; +using LiteCharms.Features.MidrandBooks.Products; +using LiteCharms.Features.MidrandBooks.Seed.Configuration; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService, + IOptions options, ILogger logger) : BackgroundService +{ + private readonly CdnSettings cdnSettings = options.Value; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + logger.LogInformation("Product Seeding started"); + + if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0) + { + logger.LogWarning("No book covers found in CDN settings. Seeding aborted."); + return; + } + + // Initialize Bogus Faker engine + var faker = new Faker(); + var culture = CultureInfo.InvariantCulture; + + // Ensure repeatable data sets if run multiple times by anchoring the seed + Randomizer.Seed = new Random(42); + + foreach (var bookCover in cdnSettings.BookCovers) + { + if (stoppingToken.IsCancellationRequested) break; + + // Generate beautifully mixed eclectic topics on the fly + var bookTopic = faker.PickRandom( + // --- Tech & IT --- + "C# 12 & Modern .NET Architecture", + "PostgreSQL Database Optimization", + "Docker & Kubernetes in Production", + "Domain-Driven Design Paradigms", + "Artificial Intelligence with Python", + + // --- Sci-Fi & Fantasy --- + "The Chronicles of the Quantum Nebula", + "Legends of the Lost Cybernetic Kingdom", + "Parallel Dimensions and Rogue Time Streams", + "The Last Android in Neo-Johannesburg", + + // --- Thrillers, Mystery & Crime --- + "The Midnight Code Cryptograph", + "Shadows in the Highveld", + "The Silent Witness of Midrand", + "Deception on the 14th Floor", + + // --- Business, Finance & Wealth --- + "Mastering the South African Tech Market", + "The Modern Entrepreneur's Blueprint", + "Generational Wealth and Venture Capital", + "Negotiation Tactics for High-Stakes Deals", + + // --- Self-Help & Personal Growth --- + "The Art of Relentless Focus", + "Building High-Performance Habits", + "The Mindfulness Guide for Software Engineers", + "Unlocking Creative Flow Under Pressure" + ); + + // Defensive Length Processing to avoid Entity Framework / Postgres string truncation crashes + var rawTitle = $"{faker.Company.CatchPhrase()} with {bookTopic}"; + var bookTitle = rawTitle.Length > 255 ? rawTitle[..252] + "..." : rawTitle; + + var rawSummary = $"A comprehensive guide to mastering {bookTopic}. Learn modern implementation techniques through real-world software engineering paradigms."; + var bookSummary = rawSummary.Length > 512 ? rawSummary[..509] + "..." : rawSummary; + + // Generating a single concise paragraph ensures a rich text description falling safely well under 1024 + var rawDescription = faker.Lorem.Paragraph(3); + var bookDescription = rawDescription.Length > 1024 ? rawDescription[..1021] + "..." : rawDescription; + + var authorFirstName = faker.Name.FirstName(); + var authorLastName = faker.Name.LastName(); + var publisherCompany = faker.Company.CompanyName(); + + // Step 1: Add Product + var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct + { + Name = bookTitle, + Summary = bookSummary, + Description = bookDescription, + ImageUrl = $"{cdnSettings.BaseCdn}{bookCover}", + Type = ProductTypes.Book, + Metadata = new Models.ProductMetadata + { + CopyrightInfo = $"© {DateTime.UtcNow.Year} {publisherCompany}. All rights reserved.", + ManufactureDate = faker.Date.Past(3).ToString("yyyy-MM-dd", culture), + Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}", + SerialNumber = faker.Phone.PhoneNumber("978-##########") + }, + Categories = ["Coding", "Computers", "IT"] + }, stoppingToken); + + if (productCreateResult.IsFailed) + { + logger.LogError("Failed to create product: {Error}", productCreateResult.Errors[0].Message); + break; + } + + // Step 2: Enable product so it can show on the shop + var enableProductResult = await productService.UpdateProductStatusAsync(productId: productCreateResult.Value, isEnabled: true, stoppingToken); + + if (enableProductResult.IsFailed) + { + logger.LogError("Failed to enable created product: {Error}", enableProductResult.Errors[0].Message); + break; + } + + // Step 3: Create Product Price + var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice + { + // Generates fair, dynamic prices in Rands between R150 and R650, snapped neatly to integers + Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2), + Discount = 0.0m + }, stoppingToken); + + if (productPriceCreateResult.IsFailed) + { + logger.LogError("Failed to create product price: {Error}", productPriceCreateResult.Errors[0].Message); + break; + } + + // Step 4: Create Author + var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor + { + Name = authorFirstName, + LastName = authorLastName, + Company = publisherCompany, + VatNumber = faker.Random.Bool() ? faker.Phone.PhoneNumber("4#########") : "", + PublisherType = faker.PickRandom(), + Email = faker.Internet.Email(authorFirstName, authorLastName), + Website = faker.Internet.Url(), + ImageUrl = faker.Internet.Avatar(), + SocialMedia = + [ + new Models.SocialMedia + { + Name = "LinkedIn", + ImageUrl = "https://cdn.example.com/icons/linkedin.png", + Type = SocialMediaTypes.LinkedIn, + Url = $"https://linkedin.com/in/{authorFirstName.ToLower(culture)}-{authorLastName.ToLower(culture)}" + }, + new Models.SocialMedia + { + Name = "GitHub", + ImageUrl = "https://cdn.example.com/icons/github.png", + Type = SocialMediaTypes.GitHub, + Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}" + } + ], + Biography = $"{authorFirstName} {authorLastName} is a veteran technologist and systems architect with over a decade of domain expertise. " + faker.Lorem.Paragraph(2), + ThumbnailImageUrl = null + }, stoppingToken); + + if (authorCreateResult.IsFailed) + { + logger.LogError("Failed to create author: {Error}", authorCreateResult.Errors[0].Message); + break; + } + + // Step 5: Create Author-Book link (product linkage) + var authorBookCreateResult = await booksService.CreateBookAsync(authorId: authorCreateResult.Value, productId: productCreateResult.Value, stoppingToken); + + if (authorBookCreateResult.IsFailed) + { + logger.LogError("Failed to create author-book linkage: {Error}", authorBookCreateResult.Errors[0].Message); + break; + } + + var enableAuthorBookResult = await booksService.UpdateBookStatusAsync(bookId: authorBookCreateResult.Value, isEnabled: true, stoppingToken); + + if (enableAuthorBookResult.IsFailed) + { + logger.LogError("Failed to enable author-book link: {Error}", enableAuthorBookResult.Errors[0].Message); + break; + } + + logger.LogInformation("Successfully seeded book product: {Title}", bookTitle); + } + + logger.LogInformation("Product 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 new file mode 100644 index 0000000..f31cdd3 --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/Program.cs @@ -0,0 +1,21 @@ +using LiteCharms.Features.MidrandBooks.Extensions; +using LiteCharms.Features.MidrandBooks.Seed; +using LiteCharms.Features.MidrandBooks.Seed.Configuration; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Configuration + .AddJsonFile("appsettings.json") + .AddUserSecrets(typeof(Program).Assembly); + +builder.Services + .AddLogging() + .AddShopServices() + .AddHostedService() + .AddMidrandShopDatabase(builder.Configuration); + +builder.Services.Configure(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options)); + +using var host = builder.Build(); + +await host.RunAsync(); \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json new file mode 100644 index 0000000..1dc7baa --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -0,0 +1,28 @@ +{ + "CdnSettings": { + "BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/", + "BookCovers": [ + "144e0314-0bd8-4e2c-8814-2b34608c0600_1764780116467.webp", + "2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp", + "3cd172b9-416e-4b3a-b613-7ff0a3aecc4c_1764780129459.webp", + "762947b6-63ac-436e-98fb-91dd94de1d82_1764780126141.webp", + "7b42f23f-666c-4dd9-82e1-9b928e1b4db7_1764780186376.webp", + "8e281eea-8910-473d-b650-43f539995d9e_1764780152316.webp", + "91d18e2c-6ee6-44b4-84a5-8692db5dbfe4_1764780114484.webp", + "94fdf403-4d10-4cb4-a537-fc914a1f5ba8_1764780198423.webp", + "9b881786-75e1-47f7-9bc9-27188d46d1ec_1764780122458.webp", + "c417236d-a628-45eb-82c2-ec1cb0326e4f_1765006317965.webp", + "clpqjsgi71jpr1i6392e449l2.webp", + "clps1mk9f0m1z1i32grvq17tg.webp", + "clq550xxy2dab1ywb6mdv4acv.webp", + "clq5535o52dj51yw557ml49c1.webp", + "clq55455w2dcm1yvd8d8258d8.webp", + "clq558fkh2djy1ywbghwcawnh.webp", + "clq55cvqh2dqi1yyratl123wq.webp", + "clrhnk94e115k1y00bg8h9ixb.webp", + "d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp", + "e6ba52f208914285bcdf1966cfb08f6f.jpg", + "fa9cbbe6-f947-4f83-8e98-61d2661f43e0_1764841636705.webp" + ] + } +} diff --git a/LiteCharms.Features/Enums.cs b/LiteCharms.Features/Enums.cs index 9cff19c..40aabdc 100644 --- a/LiteCharms.Features/Enums.cs +++ b/LiteCharms.Features/Enums.cs @@ -93,7 +93,8 @@ public enum SocialMediaTypes : int YouTube = 5, Pinterest = 6, Reddit = 7, - Tumblr = 8 + Tumblr = 8, + GitHub = 9 } public enum EmailStatuses : int diff --git a/LiteCharmsShared.slnx b/LiteCharmsShared.slnx index 1945799..be7cb5c 100644 --- a/LiteCharmsShared.slnx +++ b/LiteCharmsShared.slnx @@ -9,6 +9,7 @@ +