diff --git a/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs new file mode 100644 index 0000000..6814a6d --- /dev/null +++ b/LiteCharms.Features.MidrandBooks.Seed/CustomerSeederService.cs @@ -0,0 +1,275 @@ +using LiteCharms.Features.MidrandBooks.Customers; +using LiteCharms.Features.MidrandBooks.Customers.Models; +using LiteCharms.Features.MidrandBooks.Orders; +using LiteCharms.Features.MidrandBooks.Orders.Models; + +namespace LiteCharms.Features.MidrandBooks.Seed; + +public class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features, + ILogger logger) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + if (!await features.IsEnabledAsync("CustomerSeederService")) return; + + logger.LogInformation("Customer Seeding started"); + + // 1: Add shipping providers (shippingProvider IDs will be created in sequence: 1, 2, 3) + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 39, "https://www.fastway.co.za/our-services/track-your-parcel"), stoppingToken); + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.DHL, "DHL Couriers", 60, "https://www.dhl.com/za-en/home/tracking.html"), stoppingToken); + await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.PostNet, "Postnet Overnight Mail", 45, "https://www.postnet.co.za/tracker"), stoppingToken); + + // Initialize Bogus Faker engine + var faker = new Faker(); + var culture = CultureInfo.InvariantCulture; + + // Ensure repeatable datasets across executions + Randomizer.Seed = new Random(84); + + // South African Provinces array lookup helper + var southAfricanProvinces = new[] + { + "Gauteng", "Western Cape", "KwaZulu-Natal", "Eastern Cape", + "Free State", "Limpopo", "Mpumalanga", "North West", "Northern Cape" + }; + + // South African major towns matching geographic boundaries roughly + var southAfricanCities = new[] { "Midrand", "Johannesburg", "Pretoria", "Cape Town", "Durban", "Gqeberha", "Polokwane", "Nelspruit", "Bloemfontein" }; + + // Tracks sequential Address IDs added globally to the system across all loops + long addressSequenceCounter = 0; + + // 2: Create 15 customers with resources sequentially + for (int c = 0; c < 15; c++) + { + if (stoppingToken.IsCancellationRequested) break; + + // Determine if this specific iteration represents a Corporate Client or an Individual Consumer + bool isCompanyCustomer = faker.Random.Bool(0.4f); // 40% chance of seeding a corporate entity + + string customerFirstName = faker.Name.FirstName(); + string customerLastName = faker.Name.LastName(); + + string companyName = isCompanyCustomer ? faker.Company.CompanyName() : ""; + string companySuffix = isCompanyCustomer ? faker.Company.CompanySuffix() : ""; + string fullCompanyName = isCompanyCustomer ? $"{companyName} {companySuffix}" : ""; + + string customerEmail = isCompanyCustomer + ? faker.Internet.Email(firstName: companyName, provider: "co.za").ToLower(culture) + : faker.Internet.Email(customerFirstName, customerLastName).ToLower(culture); + + string customerPhone = faker.Phone.PhoneNumber("087#######"); // Corporate VOIP / Personal South African cell line format + string customerWebsite = isCompanyCustomer ? faker.Internet.Url().Replace("www.", $"www.{companyName.ToLower(culture)}.") : ""; + string customerVat = isCompanyCustomer ? faker.Phone.PhoneNumber("4#########") : ""; // SA VAT registration starts with a 4 + + // Randomly select distinct Social Media channels + var chosenSocialType = faker.PickRandom(); + string socialMediaUrl = chosenSocialType switch + { + SocialMediaTypes.LinkedIn => isCompanyCustomer ? $"https://linkedin.com/company/{companyName.ToLower(culture)}" : $"https://linkedin.com/in/{customerFirstName.ToLower(culture)}-{customerLastName.ToLower(culture)}", + SocialMediaTypes.GitHub => $"https://github.com/{(isCompanyCustomer ? "orgs/" + companyName.ToLower(culture) : customerFirstName.ToLower(culture))}", + _ => $"https://x.com/{(isCompanyCustomer ? companyName.ToLower(culture) : customerFirstName.ToLower(culture))}" + }; + + // 3: Create customer + var createCustomerResult = await customerService.CreateCustomerAsync(new CreateCustomer + { + Company = fullCompanyName, + Email = customerEmail, + Phone = customerPhone, + Website = customerWebsite, + SocialMedia = + [ + new Models.SocialMedia + { + Name = chosenSocialType.ToString(), + Type = chosenSocialType, + ImageUrl = $"https://cdn.example.com/icons/{chosenSocialType.ToString().ToLower(culture)}.png", + Url = socialMediaUrl + } + ], + VatNumber = customerVat + }, stoppingToken); + + if (createCustomerResult.IsFailed) + { + logger.LogError("Failed to create customer record at index {Index}: {Error}", c, createCustomerResult.Errors[0].Message); + break; + } + + var assignedCustomerId = createCustomerResult.Value; + + // 4: Create customer contact (only if customer is a company entity) + if (isCompanyCustomer) + { + var contactFirstName = faker.Name.FirstName(); + var contactLastName = faker.Name.LastName(); + + var createContactResult = await customerService.CreateCustomerContactAsync(assignedCustomerId, new CreateCustomerContact + { + Name = contactFirstName, + LastName = contactLastName, + Phone = faker.Phone.PhoneNumber("082#######"), // Typical South African mobile prefix format + Email = faker.Internet.Email(contactFirstName, contactLastName, provider: "company.co.za").ToLower(culture), + Type = ContactTypes.Business + }, stoppingToken); + + if (createContactResult.IsFailed) + { + logger.LogError("Failed to create company customer contact relation: {Error}", createContactResult.Errors[0].Message); + break; + } + } + + // Shared Randomizations for Regional Postal/Building details + var primaryState = faker.PickRandom(southAfricanProvinces); + var primaryCity = faker.PickRandom(southAfricanCities); + var shippingPostalCode = faker.Random.Replace("####"); + + var billingState = faker.PickRandom(southAfricanProvinces); + var billingCity = faker.PickRandom(southAfricanCities); + var billingPostalCode = faker.Random.Replace("####"); + + // 5: Create customer address - SHIPPING + var createShippingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress + { + Name = isCompanyCustomer ? "Head Office Distribution" : "My Home Residence", + BuildingType = faker.PickRandom(), + Type = AddressType.Shipping, + Street = $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street", + City = primaryCity, + State = primaryState, + Country = "South Africa", + IsPrimary = true, + Enabled = true, + PostalCode = shippingPostalCode + }, stoppingToken); + + long currentCustomerShippingAddressId = 0; + if (createShippingAddressResult.IsSuccess) + { + addressSequenceCounter++; + currentCustomerShippingAddressId = addressSequenceCounter; + } + else + { + logger.LogWarning("Failed to attach Shipping address profile: {Error}", createShippingAddressResult.Errors[0].Message); + } + + // 6: Create customer address - BILLING + var createBillingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress + { + Name = isCompanyCustomer ? "Accounts Payable Department" : "Billing Address", + BuildingType = faker.PickRandom(), + Type = AddressType.Billing, + Street = isCompanyCustomer ? $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Boulevard" : $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street", + City = billingCity, + State = billingState, + Country = "South Africa", + IsPrimary = false, + Enabled = true, + PostalCode = billingPostalCode + }, stoppingToken); + + long currentCustomerBillingAddressId = 0; + if (createBillingAddressResult.IsSuccess) + { + addressSequenceCounter++; + currentCustomerBillingAddressId = addressSequenceCounter; + } + else + { + logger.LogError("Failed to attach Billing address profile: {Error}", createBillingAddressResult.Errors[0].Message); + break; + } + + // 7: Challenge Extrapolation — Create a random number of orders (0 to 4 orders) per customer + int ordersToGenerate = faker.Random.Number(0, 4); + for (int o = 0; o < ordersToGenerate; o++) + { + var deliveryInstructions = faker.PickRandom( + "Leave at reception desk", + "Please call before delivery", + "At the intercom, dial 1 then option 2", + "Leave with security guard at front gate", + "Deliver to back delivery bay" + ); + + // Use the calculated sequential Billing Address Id for order creation + var orderResult = await orderService.CreateOrderAsync( + assignedCustomerId, + new CreateOrder(currentCustomerBillingAddressId, deliveryInstructions), + stoppingToken + ); + + if (orderResult.IsFailed) + { + logger.LogWarning("Failed to create purchase order shell context: {Error}", orderResult.Errors[0].Message); + continue; + } + + long seededOrderId = orderResult.Value; + + // Build a varying array of items using valid product bounds (IDs: 0 to 21) + int lineItemsCount = faker.Random.Number(1, 5); + var itemsList = new List(); + + for (int i = 0; i < lineItemsCount; i++) + { + long randomProductId = faker.Random.Number(0, 21); + long randomProductPriceId = faker.Random.Number(0, 21); + int itemQuantity = faker.Random.Number(1, 3); + + itemsList.Add(new CreateOrderItem(randomProductId, randomProductPriceId, itemQuantity)); + } + + // Push bulk items payload into order via matching test framework signatures + var addItemsResult = await orderService.AddItemsToOrderAsync(seededOrderId, [.. itemsList], stoppingToken); + if (addItemsResult.IsFailed) + { + logger.LogWarning("Failed to link item collections to Order Id {Id}", seededOrderId); + continue; + } + + // Randomly select an order status matrix pathing + var targetedOrderStatus = faker.PickRandom(); + await orderService.UpdateOrderStatusAsync(seededOrderId, targetedOrderStatus, stoppingToken); + + // Check lifecycle workflow criteria: Attach dynamic shipping if status warrants it + if (targetedOrderStatus != OrderStatus.Pending && + targetedOrderStatus != OrderStatus.Cancelled && + targetedOrderStatus != OrderStatus.Failed && + currentCustomerShippingAddressId > 0) + { + // Select from seeded Shipping Providers in step 1 (IDs: 1, 2, or 3) + long randomShippingProviderId = faker.Random.Number(1, 3); + + var addShippingResult = await orderService.AddShippingToOrderAsync( + seededOrderId, + new CreateShipping(currentCustomerShippingAddressId, randomShippingProviderId), + stoppingToken + ); + + if (addShippingResult.IsSuccess) + { + long assignedShippingId = addShippingResult.Value; + + // Transition logistics flags matching delivery metrics + var shippingStatus = faker.PickRandom(); + await orderService.UpdateShippingStatusAsync(seededOrderId, shippingStatus, stoppingToken); + + if (shippingStatus == ShippingStatuses.Shipped || shippingStatus == ShippingStatuses.Delivered) + { + string rawTrackingCode = $"ZA{faker.Random.Replace("#########")}NV"; + await orderService.UpdateShippingTrackingNumberAsync(seededOrderId, assignedShippingId, rawTrackingCode); + } + } + } + } + + logger.LogInformation("Successfully seeded customer profile #{Index}: {Name} alongside {Count} orders.", c, isCompanyCustomer ? fullCompanyName : $"{customerFirstName} {customerLastName}", ordersToGenerate); + } + + logger.LogInformation("Customer Seeding completed successfully."); + } +} \ No newline at end of file diff --git a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj index 14e2ffd..b9be4ff 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj +++ b/LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj @@ -11,10 +11,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -84,7 +85,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + @@ -95,8 +96,8 @@ - - + + @@ -128,6 +129,7 @@ + diff --git a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs index 9dfc582..868a454 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/ProductsSeederService.cs @@ -6,12 +6,14 @@ 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 + IFeatureManager features, IOptions options, ILogger 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) diff --git a/LiteCharms.Features.MidrandBooks.Seed/Program.cs b/LiteCharms.Features.MidrandBooks.Seed/Program.cs index f31cdd3..01b4fbf 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/Program.cs +++ b/LiteCharms.Features.MidrandBooks.Seed/Program.cs @@ -5,13 +5,18 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration; var builder = Host.CreateApplicationBuilder(args); builder.Configuration - .AddJsonFile("appsettings.json") - .AddUserSecrets(typeof(Program).Assembly); + .AddCommandLine(args) + .AddUserSecrets(typeof(Program).Assembly) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables(); + +builder.Services.AddScopedFeatureManagement(); builder.Services .AddLogging() .AddShopServices() .AddHostedService() + .AddHostedService() .AddMidrandShopDatabase(builder.Configuration); builder.Services.Configure(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options)); diff --git a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json index ce89459..b710f54 100644 --- a/LiteCharms.Features.MidrandBooks.Seed/appsettings.json +++ b/LiteCharms.Features.MidrandBooks.Seed/appsettings.json @@ -1,4 +1,8 @@ { + "FeatureManagement": { + "CustomerSeederService": false, + "ProductsSeederService": false + }, "CdnSettings": { "BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/", "BookCovers": [ @@ -257,5 +261,5 @@ "thumbnails/book_thumbnail_199.jpg", "thumbnails/book_thumbnail_200.jpg" ] - } + } } \ No newline at end of file