midrandshop #46
@@ -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<CustomerSeederService> 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<SocialMediaTypes>();
|
||||
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<AddressBuildingTypes>(),
|
||||
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<AddressBuildingTypes>(),
|
||||
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<CreateOrderItem>();
|
||||
|
||||
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<OrderStatus>();
|
||||
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<ShippingStatuses>();
|
||||
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.");
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,11 @@
|
||||
<!-- Quartz Scheduler-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.5" />
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.96">
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.98">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="4.5.0" />
|
||||
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
|
||||
<PackageReference Include="Quartz" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.Plugins" Version="3.18.1" />
|
||||
@@ -84,7 +85,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="Npgsql" />
|
||||
@@ -95,8 +96,8 @@
|
||||
|
||||
<!-- Email -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" Version="4.16.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.16.0" />
|
||||
<PackageReference Include="MailKit" Version="4.17.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.17.0" />
|
||||
|
||||
<!-- Global Usings-->
|
||||
<Using Include="MimeKit" />
|
||||
@@ -128,6 +129,7 @@
|
||||
<!-- Shared Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="Bogus" />
|
||||
<Using Include="Microsoft.FeatureManagement" />
|
||||
<Using Include="System.Globalization" />
|
||||
<Using Include="System.Reflection" />
|
||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||
|
||||
@@ -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<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
||||
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)
|
||||
|
||||
@@ -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<ProductsSeederService>()
|
||||
.AddHostedService<CustomerSeederService>()
|
||||
.AddMidrandShopDatabase(builder.Configuration);
|
||||
|
||||
builder.Services.Configure<CdnSettings>(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options));
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user