246 lines
12 KiB
C#
246 lines
12 KiB
C#
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,
|
|
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
|
{
|
|
private readonly CdnSettings cdnSettings = options.Value;
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
if (await features.IsEnabledAsync("ProductsSeederService") is not true) return;
|
|
|
|
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"
|
|
);
|
|
|
|
// Dynamic raw title generation formulas executed via random function picker
|
|
var titlePatterns = new Func<string>[]
|
|
{
|
|
() => $"{faker.Company.CatchPhrase()} with {bookTopic}",
|
|
() => $"The {faker.Commerce.ProductAdjective()} Guide to {bookTopic}",
|
|
() => $"Mastering {bookTopic}: A {faker.Company.Bs()} Blueprint",
|
|
() => $"{bookTopic} for the Modern {faker.Name.JobTitle()}",
|
|
() => $"Advanced {bookTopic}: Demystifying the {faker.Company.CatchPhrase()}",
|
|
() => $"{faker.Random.Replace("###")} Blueprints for {bookTopic}"
|
|
};
|
|
|
|
// Pick a format template and resolve it down to raw string text
|
|
var rawTitle = faker.PickRandom(titlePatterns)();
|
|
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();
|
|
|
|
// Safe bounded random picking for book thumbnails
|
|
string? pickedBookThumbnail = null;
|
|
string? pickedBookThumbnail1 = null;
|
|
string? pickedBookThumbnail2 = null;
|
|
string? pickedBookThumbnail3 = null;
|
|
string? pickedBookThumbnail4 = null;
|
|
if (cdnSettings.BookThumbnails is not null && cdnSettings.BookThumbnails.Length > 0)
|
|
{
|
|
pickedBookThumbnail = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
|
pickedBookThumbnail1 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
|
pickedBookThumbnail2 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
|
pickedBookThumbnail3 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
|
pickedBookThumbnail4 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
|
}
|
|
|
|
// 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"],
|
|
ThumbnailUrls = pickedBookThumbnail is not null ? [pickedBookThumbnail, pickedBookThumbnail1!, pickedBookThumbnail2!, pickedBookThumbnail3!, pickedBookThumbnail4!] : null
|
|
}, 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
|
|
{
|
|
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;
|
|
}
|
|
|
|
// Safe bounded picking for Authors (Real Avatars)
|
|
string authorAvatarUrl = faker.Internet.Avatar(); // Fallback
|
|
if (cdnSettings.Authors is not null && cdnSettings.Authors.Length > 0)
|
|
{
|
|
authorAvatarUrl = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.Authors)}";
|
|
}
|
|
|
|
// Safe bounded picking for Author Thumbnails (Cartoon Avatars)
|
|
string? authorThumbnailUrl = null;
|
|
if (cdnSettings.AuthorThumbnails is not null && cdnSettings.AuthorThumbnails.Length > 0)
|
|
{
|
|
var selectedThumb = faker.PickRandom(cdnSettings.AuthorThumbnails);
|
|
authorThumbnailUrl = $"{cdnSettings.BaseCdn}{selectedThumb}.jpg";
|
|
}
|
|
|
|
// Synthesize a highly dynamic, organic opening bio statement
|
|
var professionalBackgrounds = new[]
|
|
{
|
|
$"{authorFirstName} {authorLastName} is an award-winning {faker.Name.JobDescriptor()} {faker.Name.JobTitle()} with over {faker.Random.Number(5, 25)} years of core engineering domain expertise.",
|
|
$"As a veteran systems consultant and practicing {faker.Name.JobTitle()}, {authorFirstName} has spent decades leading digital infrastructure transformations and managing complex topologies.",
|
|
$"Operating from modern innovation hubs, {authorFirstName} {authorLastName} specializes in global product strategies and serves as an authority in {faker.Name.JobDescriptor()} computing.",
|
|
$"With a rich professional background as a principal {faker.Name.JobTitle()} at {publisherCompany}, {authorFirstName} has spent a lifetime refining the system workflows highlighted here."
|
|
};
|
|
|
|
// Pick a randomized context hook and append a 2-paragraph contextual narrative block
|
|
var biographyPrefix = faker.PickRandom(professionalBackgrounds);
|
|
var authorBiography = $"{biographyPrefix} {faker.Lorem.Paragraph(2)}";
|
|
|
|
// 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<PublisherTypes>(),
|
|
Email = faker.Internet.Email(authorFirstName, authorLastName),
|
|
Website = faker.Internet.Url(),
|
|
ImageUrl = authorAvatarUrl,
|
|
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 = authorBiography,
|
|
ThumbnailImageUrl = authorThumbnailUrl
|
|
}, 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.");
|
|
}
|
|
} |