Compare commits

...

49 Commits

Author SHA1 Message Date
khwezi 0ab14d8b63 Merge pull request 'midrandshop' (#46) from midrandshop into master
Reviewed-on: #46
2026-05-30 00:14:10 +02:00
Khwezi Mngoma 2db3b3d293 Added customer seeder with order data
continuous-integration/drone/pr Build is passing
2026-05-30 00:11:19 +02:00
Khwezi Mngoma 50eee03dbe Product seeding completed 2026-05-29 23:02:06 +02:00
khwezi 466458e230 Merge pull request 'Implemented Product Data Seeder' (#45) from midrandshop into master
Reviewed-on: #45
2026-05-29 18:56:45 +02:00
Khwezi Mngoma 60fcc70e98 Implemented Product Data Seeder
continuous-integration/drone/pr Build is passing
2026-05-29 18:56:08 +02:00
khwezi 141d32f591 Merge pull request 'Refactored migrations' (#44) from midrandshop into master
Reviewed-on: #44
2026-05-29 09:03:27 +02:00
khwezi d9e7f225ae Merge pull request 'midrandshop' (#43) from midrandshop into master
Reviewed-on: #43
2026-05-29 08:23:15 +02:00
khwezi 6ae63e2ad1 Merge pull request 'midrandshop' (#42) from midrandshop into master
Reviewed-on: #42
2026-05-27 09:15:44 +02:00
khwezi f5efdde37c Merge pull request 'Refactored connection string name' (#41) from midrandshop into master
Reviewed-on: #41
2026-05-24 13:51:11 +02:00
khwezi 1592d5dc8f Merge pull request 'midrandshop' (#40) from midrandshop into master
Reviewed-on: #40
2026-05-24 13:43:22 +02:00
khwezi 50a8a59d92 Merge pull request 'Split Features to create space for more projects' (#39) from midrandshop into master
Reviewed-on: #39
2026-05-24 13:19:55 +02:00
khwezi b70d9559b0 Merge pull request 'midrandshop' (#38) from midrandshop into master
Reviewed-on: #38
2026-05-23 11:55:02 +02:00
khwezi 81d5e8f07c Merge pull request 'Added MidrandShop feature and spl;it extensions and healthchecks' (#37) from midrandshop into master
Reviewed-on: #37
2026-05-23 11:49:27 +02:00
khwezi 20a53942b5 Merge pull request 'Added product metadata' (#36) from s3service into master
Reviewed-on: #36
2026-05-20 21:15:01 +02:00
khwezi 9edb2aa4aa Merge pull request 'Optimised UploadFileAsync()' (#35) from s3service into master
Reviewed-on: #35
2026-05-20 15:33:47 +02:00
khwezi 6ed023f2cf Merge pull request 'Refactored the S3 services to properly upload the file' (#34) from s3service into master
Reviewed-on: #34
2026-05-20 08:03:43 +02:00
khwezi 2c9f5a846c Merge pull request 'Updated how i use configs' (#33) from s3service into master
Reviewed-on: #33
2026-05-19 14:57:59 +02:00
khwezi 41f7c05be3 Merge pull request 'Refactored service to internalise the CDN' (#32) from s3service into master
Reviewed-on: #32
2026-05-19 11:34:51 +02:00
khwezi 1a03355e84 Merge pull request 'Added S3 support' (#31) from s3service into master
Reviewed-on: #31
2026-05-19 10:24:05 +02:00
khwezi 7743c3178e Merge pull request 'Simplified notification updating' (#30) from emailjobs into master
Reviewed-on: #30
2026-05-17 16:01:24 +02:00
khwezi ab3d8e6e9a Merge pull request 'Refactored GetNotificationsAsync() date handling' (#29) from emailjobs into master
Reviewed-on: #29
2026-05-17 13:14:01 +02:00
khwezi db4c348288 Merge pull request 'Fixed email sending logic' (#28) from emailjobs into master
Reviewed-on: #28
2026-05-16 00:29:01 +02:00
khwezi 6683234642 Merge pull request 'Refactored batch drop logic' (#27) from emailjobs into master
Reviewed-on: #27
2026-05-16 00:05:51 +02:00
khwezi 6ddbb9479a Merge pull request 'Added an empty constructor to ProcessEmailNotificationEvent' (#26) from emailjobs into master
Reviewed-on: #26
2026-05-15 23:53:09 +02:00
khwezi 6c7349a0f8 Merge pull request 'Added additional logging and traces' (#25) from emailjobs into master
Reviewed-on: #25
2026-05-15 23:21:52 +02:00
khwezi e97fd6cd3f Merge pull request 'Added debug logging' (#24) from emailjobs into master
Reviewed-on: #24
2026-05-15 23:09:21 +02:00
khwezi 184c7c252a Merge pull request 'Set misfireThreshold to 2min and eased Cluster node checkin limit' (#23) from emailjobs into master
Reviewed-on: #23
2026-05-15 22:29:08 +02:00
khwezi bfe8c458d6 Merge pull request 'Optimised quartz' (#22) from emailjobs into master
Reviewed-on: #22
2026-05-15 09:52:06 +02:00
khwezi e6e0475db1 Merge pull request 'emailjobs' (#21) from emailjobs into master
Reviewed-on: #21
2026-05-15 08:39:36 +02:00
khwezi 5090c60797 Merge pull request 'Fixed Lead->Customer Relationship' (#20) from emailjobs into master
Reviewed-on: #20
2026-05-15 07:55:56 +02:00
khwezi 9432252e15 Merge pull request 'Added khongisa host entry on pipeline commands' (#19) from emailjobs into master
Reviewed-on: #19
2026-05-14 02:54:00 +02:00
khwezi 47111a1a3a Merge pull request 'emailjobs' (#18) from emailjobs into master
Reviewed-on: #18
2026-05-14 02:49:34 +02:00
khwezi 6eb3d50375 Merge pull request 'Updated job scheduler' (#17) from emailjobs into master
Reviewed-on: #17
2026-05-10 17:33:23 +02:00
khwezi 4deb732804 Merge pull request 'emailjobs' (#16) from emailjobs into master
Reviewed-on: #16
2026-05-10 16:51:24 +02:00
khwezi 20d9387d0b Merge pull request 'Migrated database changes' (#15) from develop into master
Reviewed-on: #15
2026-05-10 11:17:54 +02:00
khwezi 9f6d0ccaa0 Merge pull request 'Populated README' (#14) from develop into master
Reviewed-on: #14
2026-05-10 09:48:35 +02:00
khwezi 1acbc4d213 Merge pull request 'fixed git repo naming' (#13) from develop into master
Reviewed-on: #13
2026-05-10 09:22:37 +02:00
khwezi 8c99668fac Merge pull request 'Added tag and release step after publish' (#12) from develop into master
Reviewed-on: #12
2026-05-10 09:19:33 +02:00
khwezi ad44f46204 Merge pull request 'Refactored Quartz instance id to AUTO, removed constant' (#11) from develop into master
Reviewed-on: #11
2026-05-10 08:21:08 +02:00
khwezi 49d999c1e3 Merge pull request 'Refactored postgres extension' (#10) from develop into master
Reviewed-on: #10
2026-05-09 17:45:00 +02:00
khwezi 9ed4777a18 Merge pull request 'Refactored database references' (#9) from develop into master
Reviewed-on: #9
2026-05-09 17:01:02 +02:00
khwezi 0cf44f68cc Merge pull request 'Added scheduler constants' (#8) from develop into master
Reviewed-on: #8
2026-05-09 15:27:50 +02:00
khwezi 41ed5a4288 Merge pull request 'Fixed quartz host config table prefix' (#7) from develop into master
Reviewed-on: #7
2026-05-09 13:30:33 +02:00
khwezi bbcba5e06c Merge pull request 'Fixed quartz table name prefix' (#6) from develop into master
Reviewed-on: #6
2026-05-09 13:28:29 +02:00
khwezi 502cc326dd Merge pull request 'Updated pipeline to use major version with minor always 0' (#5) from develop into master
Reviewed-on: #5
2026-05-09 12:02:19 +02:00
khwezi 4675d4c5fc Merge pull request 'Updated pipeline to use major versions only' (#4) from develop into master
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
Reviewed-on: #4
2026-05-09 11:56:20 +02:00
khwezi f80bb2fff9 Merge pull request 'Changed target branch from nain to master' (#3) from dronepipeline into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: #3
2026-05-09 11:37:39 +02:00
khwezi 6767906b0d Merge pull request 'Added pipeline separator' (#2) from dronepipeline into master
Reviewed-on: #2
2026-05-09 11:25:34 +02:00
khwezi a344af4498 Merge pull request 'Added .drone.yml pipeline' (#1) from dronepipeline into master
Reviewed-on: #1
2026-05-09 11:12:38 +02:00
8 changed files with 989 additions and 1 deletions
@@ -0,0 +1,14 @@
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
public class CdnSettings
{
public string? BaseCdn { get; set; }
public string[]? BookCovers { get; set; }
public string[]? Authors { get; set; }
public string[]? AuthorThumbnails { get; set; }
public string[]? BookThumbnails { get; set; }
}
@@ -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.");
}
}
@@ -0,0 +1,160 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>5c3bc894-8654-4691-99e8-f90d3414843f</UserSecretsId>
</PropertyGroup>
<!-- Quartz Scheduler-->
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.5" />
<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" />
<PackageReference Include="Quartz.Plugins.TimeZoneConverter" Version="3.18.1" />
<PackageReference Include="Quartz.Serialization.SystemTextJson" Version="3.18.1" />
<PackageReference Include="Quartz.AspNetCore" Version="3.18.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
<!-- Global Usings -->
<Using Include="Quartz" />
</ItemGroup>
<!-- Configuration -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.8" />
<!-- Global Usings -->
<Using Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<!-- Health Checks -->
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Core" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Data" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.8" />
<!-- Global Usings -->
<Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
<Using Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
</ItemGroup>
<!-- Open Telemetry -->
<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
<!-- Global Usings -->
<Using Include="OpenTelemetry.Resources" />
<Using Include="OpenTelemetry.Exporter" />
<Using Include="OpenTelemetry.Logs" />
<Using Include="OpenTelemetry.Metrics" />
<Using Include="OpenTelemetry.Trace" />
</ItemGroup>
<!-- Database -->
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<!-- Global Usings -->
<Using Include="Npgsql" />
<Using Include="Microsoft.EntityFrameworkCore" />
<Using Include="Microsoft.EntityFrameworkCore.Design" />
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
</ItemGroup>
<!-- Email -->
<ItemGroup>
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="MimeKit" Version="4.17.0" />
<!-- Global Usings-->
<Using Include="MimeKit" />
<Using Include="MailKit.Net.Smtp" />
</ItemGroup>
<!-- CQRS -->
<ItemGroup>
<PackageReference Include="FluentResults" Version="4.0.0" />
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
<!-- Global Usings -->
<Using Include="FluentResults" />
<Using Include="Mediator" />
</ItemGroup>
<!-- Amazon S3 SDK -->
<ItemGroup>
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.4.1" />
<PackageReference Include="AWSSDK.S3" Version="4.0.23.4" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
<!-- global Usings -->
<Using Include="Amazon.S3" />
<Using Include="Amazon.S3.Model" />
<Using Include="Amazon.Runtime" />
</ItemGroup>
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Bogus" />
<Using Include="Microsoft.FeatureManagement" />
<Using Include="System.Globalization" />
<Using Include="System.Reflection" />
<Using Include="Microsoft.AspNetCore.Builder" />
<Using Include="Microsoft.Extensions.Hosting" />
<Using Include="System.Text" />
<Using Include="System.Text.Json" />
<Using Include="System.Threading.Channels" />
<Using Include="System.Collections.ObjectModel" />
<Using Include="System.Diagnostics" />
<Using Include="System.Diagnostics.Metrics" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="System.Security.Cryptography" />
<Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Features.MidrandBooks\LiteCharms.Features.MidrandBooks.csproj" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,246 @@
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.");
}
}
@@ -0,0 +1,26 @@
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Seed;
using LiteCharms.Features.MidrandBooks.Seed.Configuration;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.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));
using var host = builder.Build();
await host.RunAsync();
@@ -0,0 +1,265 @@
{
"FeatureManagement": {
"CustomerSeederService": false,
"ProductsSeederService": false
},
"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"
],
"Authors": [
"authors/uifaces-human-avatar.jpg",
"authors/uifaces-human-avatar-1.jpg",
"authors/uifaces-human-avatar-2.jpg",
"authors/uifaces-human-avatar-3.jpg",
"authors/uifaces-human-avatar-4.jpg",
"authors/uifaces-human-avatar-5.jpg",
"authors/uifaces-human-avatar-6.jpg",
"authors/uifaces-human-avatar-7.jpg",
"authors/uifaces-human-avatar-8.jpg",
"authors/uifaces-human-avatar-9.jpg",
"authors/uifaces-human-avatar-10.jpg",
"authors/uifaces-human-avatar-11.jpg",
"authors/uifaces-human-avatar-12.jpg",
"authors/uifaces-human-avatar-13.jpg",
"authors/uifaces-human-avatar-14.jpg",
"authors/uifaces-human-avatar-15.jpg",
"authors/uifaces-human-avatar-16.jpg"
],
"AuthorThumbnails": [
"authors/thumbnails/uifaces-cartoon-avatar-1",
"authors/thumbnails/uifaces-cartoon-avatar-2",
"authors/thumbnails/uifaces-cartoon-avatar-3",
"authors/thumbnails/uifaces-cartoon-avatar-4",
"authors/thumbnails/uifaces-cartoon-avatar-5",
"authors/thumbnails/uifaces-cartoon-avatar-6",
"authors/thumbnails/uifaces-cartoon-avatar-7",
"authors/thumbnails/uifaces-cartoon-avatar-8",
"authors/thumbnails/uifaces-cartoon-avatar-9",
"authors/thumbnails/uifaces-cartoon-avatar-10"
],
"BookThumbnails": [
"thumbnails/book_thumbnail_001.jpg",
"thumbnails/book_thumbnail_002.jpg",
"thumbnails/book_thumbnail_003.jpg",
"thumbnails/book_thumbnail_004.jpg",
"thumbnails/book_thumbnail_005.jpg",
"thumbnails/book_thumbnail_006.jpg",
"thumbnails/book_thumbnail_007.jpg",
"thumbnails/book_thumbnail_008.jpg",
"thumbnails/book_thumbnail_009.jpg",
"thumbnails/book_thumbnail_010.jpg",
"thumbnails/book_thumbnail_011.jpg",
"thumbnails/book_thumbnail_012.jpg",
"thumbnails/book_thumbnail_013.jpg",
"thumbnails/book_thumbnail_014.jpg",
"thumbnails/book_thumbnail_015.jpg",
"thumbnails/book_thumbnail_016.jpg",
"thumbnails/book_thumbnail_017.jpg",
"thumbnails/book_thumbnail_018.jpg",
"thumbnails/book_thumbnail_019.jpg",
"thumbnails/book_thumbnail_020.jpg",
"thumbnails/book_thumbnail_021.jpg",
"thumbnails/book_thumbnail_022.jpg",
"thumbnails/book_thumbnail_023.jpg",
"thumbnails/book_thumbnail_024.jpg",
"thumbnails/book_thumbnail_025.jpg",
"thumbnails/book_thumbnail_026.jpg",
"thumbnails/book_thumbnail_027.jpg",
"thumbnails/book_thumbnail_028.jpg",
"thumbnails/book_thumbnail_029.jpg",
"thumbnails/book_thumbnail_030.jpg",
"thumbnails/book_thumbnail_031.jpg",
"thumbnails/book_thumbnail_032.jpg",
"thumbnails/book_thumbnail_033.jpg",
"thumbnails/book_thumbnail_034.jpg",
"thumbnails/book_thumbnail_035.jpg",
"thumbnails/book_thumbnail_036.jpg",
"thumbnails/book_thumbnail_037.jpg",
"thumbnails/book_thumbnail_038.jpg",
"thumbnails/book_thumbnail_039.jpg",
"thumbnails/book_thumbnail_040.jpg",
"thumbnails/book_thumbnail_041.jpg",
"thumbnails/book_thumbnail_042.jpg",
"thumbnails/book_thumbnail_043.jpg",
"thumbnails/book_thumbnail_044.jpg",
"thumbnails/book_thumbnail_045.jpg",
"thumbnails/book_thumbnail_046.jpg",
"thumbnails/book_thumbnail_047.jpg",
"thumbnails/book_thumbnail_048.jpg",
"thumbnails/book_thumbnail_049.jpg",
"thumbnails/book_thumbnail_050.jpg",
"thumbnails/book_thumbnail_051.jpg",
"thumbnails/book_thumbnail_052.jpg",
"thumbnails/book_thumbnail_053.jpg",
"thumbnails/book_thumbnail_054.jpg",
"thumbnails/book_thumbnail_055.jpg",
"thumbnails/book_thumbnail_056.jpg",
"thumbnails/book_thumbnail_057.jpg",
"thumbnails/book_thumbnail_058.jpg",
"thumbnails/book_thumbnail_059.jpg",
"thumbnails/book_thumbnail_060.jpg",
"thumbnails/book_thumbnail_061.jpg",
"thumbnails/book_thumbnail_062.jpg",
"thumbnails/book_thumbnail_063.jpg",
"thumbnails/book_thumbnail_064.jpg",
"thumbnails/book_thumbnail_065.jpg",
"thumbnails/book_thumbnail_066.jpg",
"thumbnails/book_thumbnail_067.jpg",
"thumbnails/book_thumbnail_068.jpg",
"thumbnails/book_thumbnail_069.jpg",
"thumbnails/book_thumbnail_070.jpg",
"thumbnails/book_thumbnail_071.jpg",
"thumbnails/book_thumbnail_072.jpg",
"thumbnails/book_thumbnail_073.jpg",
"thumbnails/book_thumbnail_074.jpg",
"thumbnails/book_thumbnail_075.jpg",
"thumbnails/book_thumbnail_076.jpg",
"thumbnails/book_thumbnail_077.jpg",
"thumbnails/book_thumbnail_078.jpg",
"thumbnails/book_thumbnail_079.jpg",
"thumbnails/book_thumbnail_080.jpg",
"thumbnails/book_thumbnail_081.jpg",
"thumbnails/book_thumbnail_082.jpg",
"thumbnails/book_thumbnail_083.jpg",
"thumbnails/book_thumbnail_084.jpg",
"thumbnails/book_thumbnail_085.jpg",
"thumbnails/book_thumbnail_086.jpg",
"thumbnails/book_thumbnail_087.jpg",
"thumbnails/book_thumbnail_088.jpg",
"thumbnails/book_thumbnail_089.jpg",
"thumbnails/book_thumbnail_090.jpg",
"thumbnails/book_thumbnail_091.jpg",
"thumbnails/book_thumbnail_092.jpg",
"thumbnails/book_thumbnail_093.jpg",
"thumbnails/book_thumbnail_094.jpg",
"thumbnails/book_thumbnail_095.jpg",
"thumbnails/book_thumbnail_096.jpg",
"thumbnails/book_thumbnail_097.jpg",
"thumbnails/book_thumbnail_098.jpg",
"thumbnails/book_thumbnail_099.jpg",
"thumbnails/book_thumbnail_100.jpg",
"thumbnails/book_thumbnail_101.jpg",
"thumbnails/book_thumbnail_102.jpg",
"thumbnails/book_thumbnail_103.jpg",
"thumbnails/book_thumbnail_104.jpg",
"thumbnails/book_thumbnail_105.jpg",
"thumbnails/book_thumbnail_106.jpg",
"thumbnails/book_thumbnail_107.jpg",
"thumbnails/book_thumbnail_108.jpg",
"thumbnails/book_thumbnail_109.jpg",
"thumbnails/book_thumbnail_110.jpg",
"thumbnails/book_thumbnail_111.jpg",
"thumbnails/book_thumbnail_112.jpg",
"thumbnails/book_thumbnail_113.jpg",
"thumbnails/book_thumbnail_114.jpg",
"thumbnails/book_thumbnail_115.jpg",
"thumbnails/book_thumbnail_116.jpg",
"thumbnails/book_thumbnail_117.jpg",
"thumbnails/book_thumbnail_118.jpg",
"thumbnails/book_thumbnail_119.jpg",
"thumbnails/book_thumbnail_120.jpg",
"thumbnails/book_thumbnail_121.jpg",
"thumbnails/book_thumbnail_122.jpg",
"thumbnails/book_thumbnail_123.jpg",
"thumbnails/book_thumbnail_124.jpg",
"thumbnails/book_thumbnail_125.jpg",
"thumbnails/book_thumbnail_126.jpg",
"thumbnails/book_thumbnail_127.jpg",
"thumbnails/book_thumbnail_128.jpg",
"thumbnails/book_thumbnail_129.jpg",
"thumbnails/book_thumbnail_130.jpg",
"thumbnails/book_thumbnail_131.jpg",
"thumbnails/book_thumbnail_132.jpg",
"thumbnails/book_thumbnail_133.jpg",
"thumbnails/book_thumbnail_134.jpg",
"thumbnails/book_thumbnail_135.jpg",
"thumbnails/book_thumbnail_136.jpg",
"thumbnails/book_thumbnail_137.jpg",
"thumbnails/book_thumbnail_138.jpg",
"thumbnails/book_thumbnail_139.jpg",
"thumbnails/book_thumbnail_140.jpg",
"thumbnails/book_thumbnail_141.jpg",
"thumbnails/book_thumbnail_142.jpg",
"thumbnails/book_thumbnail_143.jpg",
"thumbnails/book_thumbnail_144.jpg",
"thumbnails/book_thumbnail_145.jpg",
"thumbnails/book_thumbnail_146.jpg",
"thumbnails/book_thumbnail_147.jpg",
"thumbnails/book_thumbnail_148.jpg",
"thumbnails/book_thumbnail_149.jpg",
"thumbnails/book_thumbnail_150.jpg",
"thumbnails/book_thumbnail_151.jpg",
"thumbnails/book_thumbnail_152.jpg",
"thumbnails/book_thumbnail_153.jpg",
"thumbnails/book_thumbnail_154.jpg",
"thumbnails/book_thumbnail_155.jpg",
"thumbnails/book_thumbnail_156.jpg",
"thumbnails/book_thumbnail_157.jpg",
"thumbnails/book_thumbnail_158.jpg",
"thumbnails/book_thumbnail_159.jpg",
"thumbnails/book_thumbnail_160.jpg",
"thumbnails/book_thumbnail_161.jpg",
"thumbnails/book_thumbnail_162.jpg",
"thumbnails/book_thumbnail_163.jpg",
"thumbnails/book_thumbnail_164.jpg",
"thumbnails/book_thumbnail_165.jpg",
"thumbnails/book_thumbnail_166.jpg",
"thumbnails/book_thumbnail_167.jpg",
"thumbnails/book_thumbnail_168.jpg",
"thumbnails/book_thumbnail_169.jpg",
"thumbnails/book_thumbnail_170.jpg",
"thumbnails/book_thumbnail_171.jpg",
"thumbnails/book_thumbnail_172.jpg",
"thumbnails/book_thumbnail_173.jpg",
"thumbnails/book_thumbnail_174.jpg",
"thumbnails/book_thumbnail_175.jpg",
"thumbnails/book_thumbnail_176.jpg",
"thumbnails/book_thumbnail_177.jpg",
"thumbnails/book_thumbnail_178.jpg",
"thumbnails/book_thumbnail_179.jpg",
"thumbnails/book_thumbnail_180.jpg",
"thumbnails/book_thumbnail_181.jpg",
"thumbnails/book_thumbnail_182.jpg",
"thumbnails/book_thumbnail_183.jpg",
"thumbnails/book_thumbnail_184.jpg",
"thumbnails/book_thumbnail_185.jpg",
"thumbnails/book_thumbnail_186.jpg",
"thumbnails/book_thumbnail_187.jpg",
"thumbnails/book_thumbnail_188.jpg",
"thumbnails/book_thumbnail_189.jpg",
"thumbnails/book_thumbnail_190.jpg",
"thumbnails/book_thumbnail_191.jpg",
"thumbnails/book_thumbnail_192.jpg",
"thumbnails/book_thumbnail_193.jpg",
"thumbnails/book_thumbnail_194.jpg",
"thumbnails/book_thumbnail_195.jpg",
"thumbnails/book_thumbnail_196.jpg",
"thumbnails/book_thumbnail_197.jpg",
"thumbnails/book_thumbnail_198.jpg",
"thumbnails/book_thumbnail_199.jpg",
"thumbnails/book_thumbnail_200.jpg"
]
}
}
+2 -1
View File
@@ -93,7 +93,8 @@ public enum SocialMediaTypes : int
YouTube = 5, YouTube = 5,
Pinterest = 6, Pinterest = 6,
Reddit = 7, Reddit = 7,
Tumblr = 8 Tumblr = 8,
GitHub = 9
} }
public enum EmailStatuses : int public enum EmailStatuses : int
+1
View File
@@ -9,6 +9,7 @@
<Project Path="LiteCharms.Features/LiteCharms.Features.csproj" /> <Project Path="LiteCharms.Features/LiteCharms.Features.csproj" />
</Folder> </Folder>
<Folder Name="/tests/"> <Folder Name="/tests/">
<Project Path="LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj" Id="aa80643a-28dc-431f-b163-053a94e5c77c" />
<Project Path="LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj" Id="cac2f738-dbb5-4538-8565-3c2bd6f65259" /> <Project Path="LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj" Id="cac2f738-dbb5-4538-8565-3c2bd6f65259" />
<Project Path="LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj" Id="0e0967c2-7f28-4668-a387-2fc437ab066f" /> <Project Path="LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj" Id="0e0967c2-7f28-4668-a387-2fc437ab066f" />
<Project Path="LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj" Id="0696323f-7148-4ab9-9145-68b7b5df5415" /> <Project Path="LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj" Id="0696323f-7148-4ab9-9145-68b7b5df5415" />