Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ab14d8b63 | |||
| 2db3b3d293 | |||
| 50eee03dbe |
@@ -5,4 +5,10 @@ public class CdnSettings
|
|||||||
public string? BaseCdn { get; set; }
|
public string? BaseCdn { get; set; }
|
||||||
|
|
||||||
public string[]? BookCovers { 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,11 @@
|
|||||||
<!-- Quartz Scheduler-->
|
<!-- Quartz Scheduler-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Bogus" Version="35.6.5" />
|
<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>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="4.5.0" />
|
||||||
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
|
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
|
||||||
<PackageReference Include="Quartz" Version="3.18.1" />
|
<PackageReference Include="Quartz" Version="3.18.1" />
|
||||||
<PackageReference Include="Quartz.Plugins" Version="3.18.1" />
|
<PackageReference Include="Quartz.Plugins" Version="3.18.1" />
|
||||||
@@ -84,7 +85,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||||
|
|
||||||
<!-- Global Usings -->
|
<!-- Global Usings -->
|
||||||
<Using Include="Npgsql" />
|
<Using Include="Npgsql" />
|
||||||
@@ -95,8 +96,8 @@
|
|||||||
|
|
||||||
<!-- Email -->
|
<!-- Email -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MailKit" Version="4.16.0" />
|
<PackageReference Include="MailKit" Version="4.17.0" />
|
||||||
<PackageReference Include="MimeKit" Version="4.16.0" />
|
<PackageReference Include="MimeKit" Version="4.17.0" />
|
||||||
|
|
||||||
<!-- Global Usings-->
|
<!-- Global Usings-->
|
||||||
<Using Include="MimeKit" />
|
<Using Include="MimeKit" />
|
||||||
@@ -128,6 +129,7 @@
|
|||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Bogus" />
|
<Using Include="Bogus" />
|
||||||
|
<Using Include="Microsoft.FeatureManagement" />
|
||||||
<Using Include="System.Globalization" />
|
<Using Include="System.Globalization" />
|
||||||
<Using Include="System.Reflection" />
|
<Using Include="System.Reflection" />
|
||||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
|||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||||
|
|
||||||
public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
|
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;
|
private readonly CdnSettings cdnSettings = options.Value;
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
|
if (await features.IsEnabledAsync("ProductsSeederService") is not true) return;
|
||||||
|
|
||||||
logger.LogInformation("Product Seeding started");
|
logger.LogInformation("Product Seeding started");
|
||||||
|
|
||||||
if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0)
|
if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0)
|
||||||
@@ -65,8 +67,19 @@ public class ProductsSeederService(ProductService productService, AuthorService
|
|||||||
"Unlocking Creative Flow Under Pressure"
|
"Unlocking Creative Flow Under Pressure"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Defensive Length Processing to avoid Entity Framework / Postgres string truncation crashes
|
// Dynamic raw title generation formulas executed via random function picker
|
||||||
var rawTitle = $"{faker.Company.CatchPhrase()} with {bookTopic}";
|
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 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 rawSummary = $"A comprehensive guide to mastering {bookTopic}. Learn modern implementation techniques through real-world software engineering paradigms.";
|
||||||
@@ -80,6 +93,21 @@ public class ProductsSeederService(ProductService productService, AuthorService
|
|||||||
var authorLastName = faker.Name.LastName();
|
var authorLastName = faker.Name.LastName();
|
||||||
var publisherCompany = faker.Company.CompanyName();
|
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
|
// Step 1: Add Product
|
||||||
var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct
|
var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct
|
||||||
{
|
{
|
||||||
@@ -95,7 +123,8 @@ public class ProductsSeederService(ProductService productService, AuthorService
|
|||||||
Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}",
|
Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}",
|
||||||
SerialNumber = faker.Phone.PhoneNumber("978-##########")
|
SerialNumber = faker.Phone.PhoneNumber("978-##########")
|
||||||
},
|
},
|
||||||
Categories = ["Coding", "Computers", "IT"]
|
Categories = ["Coding", "Computers", "IT"],
|
||||||
|
ThumbnailUrls = pickedBookThumbnail is not null ? [pickedBookThumbnail, pickedBookThumbnail1!, pickedBookThumbnail2!, pickedBookThumbnail3!, pickedBookThumbnail4!] : null
|
||||||
}, stoppingToken);
|
}, stoppingToken);
|
||||||
|
|
||||||
if (productCreateResult.IsFailed)
|
if (productCreateResult.IsFailed)
|
||||||
@@ -116,7 +145,6 @@ public class ProductsSeederService(ProductService productService, AuthorService
|
|||||||
// Step 3: Create Product Price
|
// Step 3: Create Product Price
|
||||||
var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice
|
var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice
|
||||||
{
|
{
|
||||||
// Generates fair, dynamic prices in Rands between R150 and R650, snapped neatly to integers
|
|
||||||
Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2),
|
Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2),
|
||||||
Discount = 0.0m
|
Discount = 0.0m
|
||||||
}, stoppingToken);
|
}, stoppingToken);
|
||||||
@@ -127,6 +155,34 @@ public class ProductsSeederService(ProductService productService, AuthorService
|
|||||||
break;
|
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
|
// Step 4: Create Author
|
||||||
var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor
|
var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor
|
||||||
{
|
{
|
||||||
@@ -137,7 +193,7 @@ public class ProductsSeederService(ProductService productService, AuthorService
|
|||||||
PublisherType = faker.PickRandom<PublisherTypes>(),
|
PublisherType = faker.PickRandom<PublisherTypes>(),
|
||||||
Email = faker.Internet.Email(authorFirstName, authorLastName),
|
Email = faker.Internet.Email(authorFirstName, authorLastName),
|
||||||
Website = faker.Internet.Url(),
|
Website = faker.Internet.Url(),
|
||||||
ImageUrl = faker.Internet.Avatar(),
|
ImageUrl = authorAvatarUrl,
|
||||||
SocialMedia =
|
SocialMedia =
|
||||||
[
|
[
|
||||||
new Models.SocialMedia
|
new Models.SocialMedia
|
||||||
@@ -155,8 +211,8 @@ public class ProductsSeederService(ProductService productService, AuthorService
|
|||||||
Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}"
|
Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
Biography = $"{authorFirstName} {authorLastName} is a veteran technologist and systems architect with over a decade of domain expertise. " + faker.Lorem.Paragraph(2),
|
Biography = authorBiography,
|
||||||
ThumbnailImageUrl = null
|
ThumbnailImageUrl = authorThumbnailUrl
|
||||||
}, stoppingToken);
|
}, stoppingToken);
|
||||||
|
|
||||||
if (authorCreateResult.IsFailed)
|
if (authorCreateResult.IsFailed)
|
||||||
|
|||||||
@@ -5,13 +5,18 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
|||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
builder.Configuration
|
builder.Configuration
|
||||||
.AddJsonFile("appsettings.json")
|
.AddCommandLine(args)
|
||||||
.AddUserSecrets(typeof(Program).Assembly);
|
.AddUserSecrets(typeof(Program).Assembly)
|
||||||
|
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||||
|
.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
builder.Services.AddScopedFeatureManagement();
|
||||||
|
|
||||||
builder.Services
|
builder.Services
|
||||||
.AddLogging()
|
.AddLogging()
|
||||||
.AddShopServices()
|
.AddShopServices()
|
||||||
.AddHostedService<ProductsSeederService>()
|
.AddHostedService<ProductsSeederService>()
|
||||||
|
.AddHostedService<CustomerSeederService>()
|
||||||
.AddMidrandShopDatabase(builder.Configuration);
|
.AddMidrandShopDatabase(builder.Configuration);
|
||||||
|
|
||||||
builder.Services.Configure<CdnSettings>(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options));
|
builder.Services.Configure<CdnSettings>(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options));
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"FeatureManagement": {
|
||||||
|
"CustomerSeederService": false,
|
||||||
|
"ProductsSeederService": false
|
||||||
|
},
|
||||||
"CdnSettings": {
|
"CdnSettings": {
|
||||||
"BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/",
|
"BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/",
|
||||||
"BookCovers": [
|
"BookCovers": [
|
||||||
@@ -23,6 +27,239 @@
|
|||||||
"d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp",
|
"d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp",
|
||||||
"e6ba52f208914285bcdf1966cfb08f6f.jpg",
|
"e6ba52f208914285bcdf1966cfb08f6f.jpg",
|
||||||
"fa9cbbe6-f947-4f83-8e98-61d2661f43e0_1764841636705.webp"
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user