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 sealed 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."); } }