Compare commits

..

1 Commits

Author SHA1 Message Date
khwezi 92c00360a7 Merge commit '38e765203d6a6e435e4e80368ee3dbb6c4098d2a' 2026-05-29 07:03:28 +00:00
72 changed files with 136 additions and 8051 deletions
@@ -1,133 +0,0 @@
using LiteCharms.Features.MidrandBooks.Categories;
using LiteCharms.Features.MidrandBooks.Products;
namespace LiteCharms.Features.MidrandBooks.Seed;
public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
ILogger<CategorySeederService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!await features.IsEnabledAsync("CategorySeederService")) return;
logger.LogInformation("Category and Product-Tag Mapping Seeding started (15-char limit applied)");
// Initialize Bogus to ensure repeatable distribution matrix pathing
var faker = new Faker();
Randomizer.Seed = new Random(101);
// 1. Curate Broad Book Categories (IsMain = true, Max 20, Max 15 chars)
var broadMainCategories = new[]
{
"Fiction", "Non-Fiction", "Youth & Kids", "Academic",
"Biographies", "Business", "Sci-Fi & Fantasy",
"Thrillers", "Self-Help", "History",
"Spirituality", "Arts & Photo", "Technology",
"Cookbooks", "Travel & Maps", "Poetry & Drama", "Graphic Novels"
};
// 2. Curate Niche Subcategories/Tags (IsMain = false, Max 15 chars)
var specializedSubCategories = new[]
{
"Cyberpunk", "Space Opera", "Historical Fix", "Cozy Mystery", "True Crime",
"Agile Project", "Software Eng", "AI & ML", "Cloud Comput",
"SA History", "African Lit", "Apartheid Era", "Mandela Legacy",
"Finance", "Investments", "Startup", "Leadership",
"CBT Therapy", "Mindfulness", "Yoga & Health",
"Baking Basics", "African Food", "Vegan Recipes",
"Ancient World", "WWII History", "Geopolitics",
"Writing Guides", "Criticism", "Classic Poetry",
"Early Learning", "Teen Romance", "Survival",
"Urban Fantasy", "Dark Fantasy", "Psych Thriller", "Hard Sci-Fi",
"Data Science", "DevOps", "Cybersecurity",
"Economics", "Real Estate", "Governance",
"Essays", "Memoirs", "Art History",
"Architecture", "Photography", "Travel Writing",
"Gaming Culture", "Philosophy", "Ethics",
"DIY Home", "SA Gardening", "Parenting"
};
// 3. Seed Main Categories into the System
logger.LogInformation("Seeding broad main categories...");
foreach (var mainCat in broadMainCategories)
{
if (stoppingToken.IsCancellationRequested) return;
// Defensive truncation fallback just in case strings get modified later
string safeName = mainCat.Length > 15 ? mainCat.Substring(0, 15) : mainCat;
var result = await categoryService.CreateCategoryAsync(safeName, isMain: true, stoppingToken);
if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning("Notice while adding main category '{Name}': {Msg}", safeName, result.Errors[0].Message);
}
}
// 4. Seed Subcategories into the System
logger.LogInformation("Seeding boundless specialized niche tags...");
foreach (var subCat in specializedSubCategories)
{
if (stoppingToken.IsCancellationRequested) return;
string safeName = subCat.Length > 15 ? subCat.Substring(0, 15) : subCat;
var result = await categoryService.CreateCategoryAsync(safeName, isMain: false, stoppingToken);
if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning("Notice while adding subcategory '{Name}': {Msg}", safeName, result.Errors[0].Message);
}
}
// 5. Query back all enabled categories to extract active IDs for junction mapping
var fetchMainResult = await categoryService.GetCategoriesAsync(isMain: true, stoppingToken);
var fetchSubResult = await categoryService.GetCategoriesAsync(isMain: false, stoppingToken);
if (fetchMainResult.IsFailed || fetchSubResult.IsFailed)
{
logger.LogError("Aborting junction seeding: Could not retrieve categories from data store.");
return;
}
var mainCategoryIds = fetchMainResult.Value.Select(c => c.Id).ToArray();
var subCategoryIds = fetchSubResult.Value.Select(c => c.Id).ToArray();
// 6. Map Categories to your Product Collection (Product IDs 0 - 21)
logger.LogInformation("Beginning Product-Category mapping assignments for Product IDs 0 through 21...");
for (long productId = 0; productId <= 21; productId++)
{
if (stoppingToken.IsCancellationRequested) break;
// Every book belongs to 1 or 2 main categories
int mainCategoriesToAssign = faker.Random.Number(1, 2);
var chosenMainIds = faker.PickRandom(mainCategoryIds, mainCategoriesToAssign).Distinct();
foreach (var mainId in chosenMainIds)
{
var linkResult = await productService.AddProductCategoryAsync(productId, mainId, stoppingToken);
if (linkResult.IsFailed)
{
if (!linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase))
{
logger.LogDebug("Junction note for Product {PId} and Main Category {CId}: {Msg}", productId, mainId, linkResult.Errors[0].Message);
}
}
}
// Every book gets 1 to 4 granular subgenre tags
int subCategoriesToAssign = faker.Random.Number(1, 4);
var chosenSubIds = faker.PickRandom(subCategoryIds, subCategoriesToAssign).Distinct();
foreach (var subId in chosenSubIds)
{
var linkResult = await productService.AddProductCategoryAsync(productId, subId, stoppingToken);
if (linkResult.IsFailed && !linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase))
{
logger.LogDebug("Junction note for Product {PId} and Sub Category {CId}: {Msg}", productId, subId, linkResult.Errors[0].Message);
}
}
}
logger.LogInformation("Category and Product-Tag Mapping Seeding completed successfully.");
}
}
@@ -1,14 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
public sealed 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; }
}
@@ -1,275 +0,0 @@
using LiteCharms.Features.MidrandBooks.Customers;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Orders;
using LiteCharms.Features.MidrandBooks.Orders.Models;
namespace LiteCharms.Features.MidrandBooks.Seed;
public sealed class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
ILogger<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.");
}
}
@@ -1,160 +0,0 @@
<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>
@@ -1,246 +0,0 @@
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 sealed 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.");
}
}
@@ -1,27 +0,0 @@
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<CategorySeederService>()
.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();
@@ -1,266 +0,0 @@
{
"FeatureManagement": {
"CategorySeederService": true,
"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"
]
}
}
@@ -5,7 +5,7 @@ using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests; namespace LiteCharms.Features.MidrandBooks.Tests;
public class AuthorServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture> public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{ {
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>(); private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
@@ -1,70 +0,0 @@
using LiteCharms.Features.MidrandBooks.Categories;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class CategoryServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly CategoryService categoryService = fixture.Services.GetRequiredService<CategoryService>();
[IntegrationFact]
public async Task UpdateCategoryStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await categoryService.UpdateCategoryStatusAsync(3, false, false, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetCategoryAsync_ShouldReturn_ResultWithCategory()
{
var result = await categoryService.GetCategoryAsync(3, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetCategoriesAsync_ShouldReturn_All_ResultWithCategoryList()
{
var result = await categoryService.GetCategoriesAsync(isMain: null,fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCategoriesAsync_ShouldReturn_MainCategory_ResultWithCategoryList()
{
var result = await categoryService.GetCategoriesAsync(true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCategoriesAsync_ShouldReturn_SubMainCategory_ResultWithCategoryList()
{
var result = await categoryService.GetCategoriesAsync(false, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task CreateCategoriesAsync_ShouldReturn_ResultWithSuccess()
{
var result = await categoryService.CreateCategoriesAsync(fixture.CancellationToken, "Test", "Test 1", "Test 2");
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CreateCategoryAsync_ShouldReturn_ResultWithCategoryId()
{
var result = await categoryService.CreateCategoryAsync("Test", true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
}
@@ -1,4 +1,5 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
namespace LiteCharms.Features.MidrandBooks.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests.Common;
@@ -1,98 +0,0 @@
using LiteCharms.Features.MidrandBooks.Payments;
using LiteCharms.Features.MidrandBooks.Payments.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public sealed class PaymentServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly PaymentService paymentService = fixture.Services.GetRequiredService<PaymentService>();
[IntegrationFact]
public async Task CreateRefundAsync_ShouldReturn_ResultWithRefundId()
{
var request = new CreateRefund
{
Amount = 50,
OrderId = 2,
Type = RefundTypes.Partial,
Reason = "Returned damaged book",
Status = RefundStatus.Completed,
};
var result = await paymentService.CreateRefundAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithSuccess()
{
var request = new CreateLedgerEntry
{
CustomerId = 1,
OrderId = 1,
PaymentGatewayId = 1,
PaymentGatewayReference = "TEST REFERENCE",
PaymentId = 1,
Status = LedgerStatuses.Received,
};
var result = await paymentService.WriteLedgerEntryAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetPaymentGatewayAsync_ShouldReturn_ResultWithPaymentGateway()
{
var result = await paymentService.GetPaymentGatewayAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task CreatePaymentGatewayAsync_ShouldReturn_ResultWithGatewayId()
{
var request = new CreatePaymentGateway
{
IsSandbox = true,
MerchantId = "10049307",
MerchantKey = "ju6navn0jcbf0",
Name = "Payfast",
Website = "https://sandbox.payfast.co.za/eng/process",
};
var result = await paymentService.CreatePaymentGatewayAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task CompletePaymentAsync_ShouldReturn_ResultWithSuccess()
{
var result = await paymentService.CompletePaymentAsync(1, PaymentStatuses.Paid, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdatePaymentAsync_ShouldReturn_ResultWithSuccess()
{
var result = await paymentService.UpdatePaymentAsync(1, 200, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CreatePaymentAsync_ShouldReturn_ResultWithPaymentId()
{
var result = await paymentService.CreatePaymentAsync(100, 1, "HASHEDID", fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
}
@@ -10,81 +10,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu
private readonly ProductService productService = fixture.Services.GetRequiredService<ProductService>(); private readonly ProductService productService = fixture.Services.GetRequiredService<ProductService>();
[IntegrationFact] [IntegrationFact]
public async Task CheckProductStockAvailabilityAsync_ShouldReturn_ResultWithProductInventory() public async Task GetProductPriceAsync_ShouldReturn_RetultOneProductPrice()
{
var result = await productService.CheckProductStockAvailabilityAsync(1, 1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task ReserveProductInventoryAsync_ShouldReturn_ResultWithSuccess()
{
var request = new ReserveStock
{
ProductId = 1,
ProductPriceId = 1,
Reservation = 100,
};
var result = await productService.ReserveProductInventoryAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task AllocateProductInventoryAsync_ShouldReturn_ResultWithSuccess()
{
var request = new AllocateStock
{
ProductId = 1,
ProductPriceId = 1,
Allocation = 500,
};
var result = await productService.AllocateProductInventoryAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId()
{
var result = await productService.AddProductCategoryAsync(1, 2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetProductCategoriesAsync_ShouldReturn_ResultWithCategoryList()
{
var result = await productService.GetProductCategoriesAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task DeleteProductCategoryAsync_ShouldReturn_ResultWithSuccess()
{
var result = await productService.DeleteProductCategoryAsync(1, 1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task DeleteAllProductCategoriesAsync_ShouldReturn_ResultWithSuccess()
{
var result = await productService.DeleteAllProductCategoriesAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetProductPriceAsync_ShouldReturn_ResultOneProductPrice()
{ {
var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken); var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken);
@@ -95,7 +21,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu
} }
[IntegrationFact] [IntegrationFact]
public async Task GetProductPricesAsync_ShouldReturn_ResultProductPriceList() public async Task GetProductPricesAsync_ShouldReturn_RetultProductPriceList()
{ {
var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken); var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken);
@@ -106,7 +32,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu
} }
[IntegrationFact] [IntegrationFact]
public async Task SearchProductsAsync_ShouldReturn_ResultMatchingProducts() public async Task SearchProductsAsync_ShouldReturn_RetultMatchingProducts()
{ {
var filter = new ProductFilter var filter = new ProductFilter
{ {
@@ -126,7 +52,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu
} }
[IntegrationFact] [IntegrationFact]
public async Task GetProductAsync_ShouldReturn_ResultOneProduct() public async Task GetProductAsync_ShouldReturn_RetultOneProduct()
{ {
var result = await productService.GetProductAsync(2, fixture.CancellationToken); var result = await productService.GetProductAsync(2, fixture.CancellationToken);
@@ -137,7 +63,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu
} }
[IntegrationFact] [IntegrationFact]
public async Task GetProductsAsync_ShouldReturn_ResultProducts() public async Task GetProductsAsync_ShouldReturn_RetultProducts()
{ {
var range = new DateRange var range = new DateRange
{ {
@@ -155,7 +81,7 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu
} }
[IntegrationFact] [IntegrationFact]
public async Task UpdateProductStatusAsync_ShouldResurn_ResultTrue() public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue()
{ {
var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken); var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken);
@@ -0,0 +1,3 @@
namespace LiteCharms.Features.MidrandBooks.Abstractions;
public interface IService;
@@ -1,4 +1,4 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
@@ -95,7 +95,7 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
.AsNoTracking() .AsNoTracking()
.Include(b => b.Author) .Include(b => b.Author)
.Include(b => b.Product) .Include(b => b.Product)
.ThenInclude(b => b!.Prices) .ThenInclude(b => b.Prices)
.OrderByDescending(b => b.CreatedAt) .OrderByDescending(b => b.CreatedAt)
.Where(b => b.AuthorId == authorId) .Where(b => b.AuthorId == authorId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -1,4 +1,4 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
@@ -8,29 +8,6 @@ namespace LiteCharms.Features.MidrandBooks.Authors;
public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{ {
public async ValueTask<Result<Author>> GetAuthorByProductIdAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var author = await context.Books
.AsNoTracking()
.Include(i => i.Author)
.Where(b => b.ProductId == productId)
.FirstOrDefaultAsync(cancellationToken);
if (author is null)
return Result.Fail<Author>(new Error($"No author association discovered for Product ID {productId}"));
return Result.Ok(author.Author!.ToModel());
}
catch (Exception ex)
{
return Result.Fail<Author>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default) public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default)
{ {
try try
@@ -1,132 +0,0 @@
using LiteCharms.Features.Abstractions;
using LiteCharms.Features.MidrandBooks.Categories.Models;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres;
namespace LiteCharms.Features.MidrandBooks.Categories;
public sealed class CategoryService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result> UpdateCategoryStatusAsync(long categoryId, bool enabled, bool isMain, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rowsUpdated = await context.Categories
.Where(c => c.Id == categoryId && c.Enabled)
.ExecuteUpdateAsync(setters => setters
.SetProperty(c => c.Enabled, enabled)
.SetProperty(c => c.IsMain, isMain), cancellationToken);
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to update category"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Category>> GetCategoryAsync(long categoryId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var category = await context.Categories.AsNoTracking().FirstOrDefaultAsync(c => c.Id == categoryId, cancellationToken);
return category is not null
? Result.Ok(category.ToModel())
: Result.Fail<Category>("Failed to create new category");
}
catch (Exception ex)
{
return Result.Fail<Category>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Category[]>> GetCategoriesAsync(bool? isMain = null, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Categories.AsNoTracking()
.OrderByDescending(o => o.IsMain)
.ThenByDescending(o => o.Id)
.ThenBy(o => o.Name)
.AsQueryable();
query = isMain is null
? query.Where(c => c.Enabled).AsQueryable()
: query.Where(c => c.Enabled && c.IsMain == isMain.Value);
var categories = await query.ToListAsync(cancellationToken);
return categories?.Count > 0
? Result.Ok(categories.Select(c => c.ToModel()).ToArray())
: Result.Fail<Category[]>("No categories found");
}
catch (Exception ex)
{
return Result.Fail<Category[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CreateCategoriesAsync(CancellationToken cancellationToken = default, params string[] categories)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
foreach (var category in categories)
{
if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken))
continue;
context.Categories.Add(new Entities.Category
{
Name = category.Humanize(LetterCasing.Title),
IsMain = false,
Enabled = true,
});
}
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to add any category in the list");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateCategoryAsync(string category, bool isMain, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken))
return Result.Fail($"Category '{category}' already exists");
var newCategory = context.Categories.Add(new Entities.Category
{
Name = StringHumanizeExtensions.Humanize(category, LetterCasing.Title),
IsMain = isMain,
Enabled = true,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(newCategory.Entity.Id)
: Result.Fail("Failed to create new category");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -1,4 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Categories.Entities;
[EntityTypeConfiguration<CategoryConfiguration, Category>]
public sealed class Category : Models.Category;
@@ -1,14 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Categories.Entities;
public sealed class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.ToTable("Categories");
builder.HasKey(c => c.Id);
builder.Property(c => c.Name).IsRequired().HasMaxLength(15);
builder.Property(c => c.IsMain).HasDefaultValue(false);
builder.Property(c => c.Enabled).HasDefaultValue(true);
}
}
@@ -1,12 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Categories.Models;
public class Category
{
public long Id { get; set; }
public string? Name { get; set; }
public bool IsMain { get; set; }
public bool Enabled { get; set; }
}
@@ -1,4 +1,4 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
+62
View File
@@ -0,0 +1,62 @@
namespace LiteCharms.Features.MidrandBooks;
public enum PublisherTypes : int
{
Individual = 0,
Company = 1,
Organization = 2,
SelfPublished = 3,
UniversityPress = 4,
GovernmentAgency = 5,
NonProfit = 6,
Independent = 7
}
public enum BookTypes : int
{
Fiction = 0,
NonFiction = 1,
Academic = 2,
SelfHelp = 3,
Biography = 4,
Poetry = 5,
Children = 6,
YoungAdult = 7,
ScienceFiction = 8,
Fantasy = 9
}
public enum BookContentTypes : int
{
Text = 0,
Image = 1,
Video = 2,
Audio = 3,
Interactive = 4,
Markdown = 5,
Html = 6,
Json = 7,
Yaml = 8
}
public enum BookPageTypes : int
{
Cover = 0,
Preface = 1,
Introduction = 2,
Content = 3,
Closing = 4,
Referencer = 5,
Credits = 6,
BackCover = 7
}
public enum ProductTypes : int
{
Book = 1,
Journal = 2,
Magazine = 3,
EBook = 4,
Audiobook = 5,
Accessory = 6
}
@@ -1,84 +1,14 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models; using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Categories.Models;
using LiteCharms.Features.MidrandBooks.Customers.Models; using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Orders.Models;
using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Pages.Models;
using LiteCharms.Features.MidrandBooks.Payments.Models;
using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.MidrandBooks.Products.Models;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace LiteCharms.Features.MidrandBooks.Extensions; namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class Mappers public static class Mappers
{ {
public static Refund ToModel(this Payments.Entities.Refund entity) => new()
{
CreatedAt = entity.CreatedAt,
Amount = entity.Amount,
Id = entity.Id,
OrderId = entity.OrderId,
Reason = entity.Reason,
Status = entity.Status,
Type = entity.Type,
UpdatedAt = entity.UpdatedAt,
};
public static PaymentLedger ToModel(this Payments.Entities.PaymentLedger entity) => new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
CustomerId = entity.CustomerId,
OrderId = entity.OrderId,
PaymentGatewayId = entity.PaymentGatewayId,
PaymentGatewayReference = entity.PaymentGatewayReference,
PaymentId = entity.PaymentId,
Status = entity.Status,
};
public static PaymentGateway ToModel(this Payments.Entities.PaymentGateway entity) => new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
Enabled = entity.Enabled,
IsSandbox = entity.IsSandbox,
MerchantId = entity.MerchantId,
MerchantKey = entity.MerchantKey,
Name = entity.Name,
Website = entity.Website,
};
public static Payment ToModel(this Payments.Entities.Payment entity) => new()
{
Id = entity.Id,
Amount = entity.Amount,
CreatedAt = entity.CreatedAt,
OrderId = entity.OrderId,
Reference = entity.Reference,
Status = entity.Status,
UpdatedAt = entity.UpdatedAt,
};
public static ProductInventory ToModel(this Products.Entities.ProductInventory entity) => new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
ProductId = entity.ProductId,
ProductPriceId = entity.ProductPriceId,
Status = entity.Status,
TotalAllocated = entity.TotalAllocated,
TotalReserved = entity.TotalReserved,
};
public static Category ToModel(this Categories.Entities.Category entity) => new()
{
Id = entity.Id,
Name = entity.Name,
IsMain = entity.IsMain,
Enabled = entity.Enabled,
};
public static ShippingProvider ToModel(this Orders.Entities.ShippingProvider entity) => new() public static ShippingProvider ToModel(this Orders.Entities.ShippingProvider entity) => new()
{ {
Id = entity.Id, Id = entity.Id,
@@ -208,21 +138,25 @@ public static class Mappers
Enabled = entity.Enabled Enabled = entity.Enabled
}; };
public static Product ToModel(this Products.Entities.Product entity) => new Product public static Product ToModel(this Products.Entities.Product entity)
{ {
Id = entity.Id, return new Product
CreatedAt = entity.CreatedAt, {
UpdatedAt = entity.UpdatedAt, Id = entity.Id,
Name = entity.Name, CreatedAt = entity.CreatedAt,
Summary = entity.Summary, UpdatedAt = entity.UpdatedAt,
Description = entity.Description, Name = entity.Name,
Type = entity.Type, Summary = entity.Summary,
ImageUrl = entity.ImageUrl, Description = entity.Description,
ThumbnailUrls = entity.ThumbnailUrls, Type = entity.Type,
Metadata = entity.Metadata, ImageUrl = entity.ImageUrl,
Enabled = entity.Enabled, ThumbnailUrls = entity.ThumbnailUrls,
Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null, Metadata = entity.Metadata,
}; Categories = entity.Categories,
Enabled = entity.Enabled,
Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null,
};
}
public static Author ToModel(this Authors.Entities.Author entity) => new() public static Author ToModel(this Authors.Entities.Author entity) => new()
{ {
@@ -1,4 +1,4 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
namespace LiteCharms.Features.MidrandBooks.Extensions; namespace LiteCharms.Features.MidrandBooks.Extensions;
@@ -31,8 +31,7 @@
<!-- Quartz Scheduler--> <!-- Quartz Scheduler-->
<ItemGroup> <ItemGroup>
<PackageReference Include="Humanizer" Version="3.0.10" /> <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>
@@ -105,7 +104,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.2" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.1" />
<!-- Global Usings --> <!-- Global Usings -->
<Using Include="Npgsql" /> <Using Include="Npgsql" />
@@ -116,8 +115,8 @@
<!-- Email --> <!-- Email -->
<ItemGroup> <ItemGroup>
<PackageReference Include="MailKit" Version="4.17.0" /> <PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="MimeKit" Version="4.17.0" /> <PackageReference Include="MimeKit" Version="4.16.0" />
<!-- Global Usings--> <!-- Global Usings-->
<Using Include="MimeKit" /> <Using Include="MimeKit" />
@@ -148,7 +147,6 @@
<!-- Shared Usings --> <!-- Shared Usings -->
<ItemGroup> <ItemGroup>
<Using Include="Humanizer" />
<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" />
@@ -1,4 +1,4 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Orders.Models; using LiteCharms.Features.MidrandBooks.Orders.Models;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
@@ -1,4 +1,4 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Pages.Models; using LiteCharms.Features.MidrandBooks.Pages.Models;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
@@ -1,9 +0,0 @@
using LiteCharms.Features.MidrandBooks.Orders.Entities;
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
[EntityTypeConfiguration<PaymentConfiguration, Payment>]
public class Payment : Models.Payment
{
public virtual Order? Order { get; set; }
}
@@ -1,22 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
public sealed class PaymentConfiguration : IEntityTypeConfiguration<Payment>
{
public void Configure(EntityTypeBuilder<Payment> builder)
{
builder.ToTable("Payments");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt);
builder.Property(f => f.Status).IsRequired();
builder.Property(f => f.Reference).IsRequired();
builder.Property(f => f.OrderId).IsRequired();
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
builder.HasOne(f => f.Order)
.WithMany()
.HasForeignKey(f => f.OrderId)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -1,4 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
[EntityTypeConfiguration<PaymentGatewayConfiguration, PaymentGateway>]
public class PaymentGateway : Models.PaymentGateway;
@@ -1,19 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration<PaymentGateway>
{
public void Configure(EntityTypeBuilder<PaymentGateway> builder)
{
builder.ToTable("Gateways");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt);
builder.Property(f => f.Website).IsRequired(false);
builder.Property(f => f.IsSandbox);
builder.Property(f => f.MerchantKey).IsRequired();
builder.Property(f => f.MerchantId).IsRequired();
builder.Property(f => f.Enabled);
builder.Property(f => f.Name).IsRequired();
}
}
@@ -1,16 +0,0 @@
using LiteCharms.Features.MidrandBooks.Customers.Entities;
using LiteCharms.Features.MidrandBooks.Orders.Entities;
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
[EntityTypeConfiguration<PaymentLedgerConfiguration, PaymentLedger>]
public class PaymentLedger : Models.PaymentLedger
{
public virtual Payment? Payment { get; set; }
public virtual Order? Order { get; set; }
public virtual Customer? Customer { get; set; }
public virtual PaymentGateway? Gateway { get; set; }
}
@@ -1,41 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
public sealed class PaymentLedgerConfiguration : IEntityTypeConfiguration<PaymentLedger>
{
public void Configure(EntityTypeBuilder<PaymentLedger> builder)
{
builder.ToTable("Ledger");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.Status).IsRequired();
builder.Property(f => f.PaymentGatewayReference).IsRequired(false);
builder.Property(f => f.PaymentGatewayId).IsRequired(false);
builder.Property(f => f.OrderId).IsRequired();
builder.Property(f => f.CustomerId).IsRequired();
builder.Property(f => f.PaymentId).IsRequired();
builder.HasOne(f => f.Payment)
.WithMany()
.IsRequired()
.HasForeignKey(f => f.PaymentId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(f => f.Order)
.WithMany()
.IsRequired()
.HasForeignKey(f => f.OrderId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(f => f.Customer)
.WithMany()
.IsRequired()
.HasForeignKey(f => f.CustomerId);
builder.HasOne(f => f.Gateway)
.WithMany()
.IsRequired(false)
.HasForeignKey(f => f.PaymentGatewayId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -1,18 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
public class Payment
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public decimal Amount { get; set; }
public long OrderId { get; set; }
public string? Reference { get; set; }
public PaymentStatuses Status { get; set; }
}
@@ -1,22 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
public class PaymentGateway
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? Name { get; set; }
public string? Website { get; set; }
public string? MerchantId { get; set; }
public string? MerchantKey { get; set; }
public bool IsSandbox { get; set; }
public bool Enabled { get; set; }
}
@@ -1,20 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
public class PaymentLedger
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public LedgerStatuses Status { get; set; }
public long OrderId { get; set; }
public long PaymentId { get; set; }
public long CustomerId { get; set; }
public string? PaymentGatewayReference { get; set; }
public long? PaymentGatewayId { get; set; }
}
@@ -1,53 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
public sealed record UpdateRefund
{
public long OrderId { get; set; }
public RefundStatus Status { get; set; }
public string? Reason { get; set; }
public decimal Amount { get; set; }
};
public sealed record CreateRefund
{
public long OrderId { get; set; }
public RefundTypes Type { get; set; }
public RefundStatus Status { get; set; }
public string? Reason { get; set; }
public decimal Amount { get; set; }
}
public sealed record CreateLedgerEntry
{
public required LedgerStatuses Status { get; set; }
public required long OrderId { get; set; }
public required long PaymentId { get; set; }
public required long CustomerId { get; set; }
public string? PaymentGatewayReference { get; set; }
public long? PaymentGatewayId { get; set; }
}
public sealed record CreatePaymentGateway
{
public required string? Name { get; set; }
public string? Website { get; set; }
public required string? MerchantId { get; set; }
public required string? MerchantKey { get; set; }
public bool IsSandbox { get; set; }
}
@@ -1,264 +1,7 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Payments.Models;
using LiteCharms.Features.MidrandBooks.Postgres;
namespace LiteCharms.Features.MidrandBooks.Payments; namespace LiteCharms.Features.MidrandBooks.Payments;
public sealed class PaymentService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService public sealed class PaymentService : IService
{ {
public async ValueTask<Result<Refund>> GetRefundAsync(long refundId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var refund = await context.Refunds.AsNoTracking()
.FirstOrDefaultAsync(r => r.Id == refundId, cancellationToken);
return refund is not null
? Result.Ok(refund.ToModel())
: Result.Fail<Refund>("Could not find refund");
}
catch (Exception ex)
{
return Result.Fail<Refund>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateRefundAsync(long refundId, UpdateRefund request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken))
return Result.Fail("Order not found");
var updatedRows = await context.Refunds
.Where(r => r.Id == refundId && r.OrderId == request.OrderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(r => r.Status, request.Status)
.SetProperty(r => r.Reason, request.Reason)
.SetProperty(r => r.UpdatedAt, DateTime.UtcNow)
.SetProperty(r => r.Amount, request.Amount), cancellationToken);
return updatedRows > 0
? Result.Ok()
: Result.Fail("Failed to update refund");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateRefundAsync(CreateRefund request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var order = await context.Orders.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == request.OrderId
&& o.Status == OrderStatus.Completed, cancellationToken);
if (order is null) return Result.Fail("Order not found");
if (request.Amount > order.Total)
return Result.Fail<long>("Refund amount cannot be greater than order total");
var totalRefundsPaid = await context.Refunds
.Where(r => r.OrderId == request.OrderId)
.SumAsync(r => r.Amount, cancellationToken);
if (request.Amount > (order.Total - totalRefundsPaid))
return Result.Fail<long>("Refund amount exceeds amount available for refund");
var refund = context.Refunds.Add(new Entities.Refund
{
Amount = request.Amount,
CreatedAt = DateTime.UtcNow,
OrderId = request.OrderId,
Reason = request.Reason,
Status = request.Status,
Type = request.Type,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(refund.Entity.Id)
: Result.Fail<long>("Failed to create refund");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> WriteLedgerEntryAsync(CreateLedgerEntry request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken))
return Result.Fail("Order not found");
if (!await context.Customers.AnyAsync(o => o.Id == request.CustomerId, cancellationToken))
return Result.Fail("Customer not found");
if (!await context.Orders.AnyAsync(oc => oc.Id == request.OrderId && oc.CustomerId == request.CustomerId, cancellationToken))
return Result.Fail("Customer does not match the order");
if (!await context.Payments.AnyAsync(o => o.Id == request.PaymentId && o.OrderId == request.OrderId, cancellationToken))
return Result.Fail("Payment not found");
if (request.PaymentGatewayId is not null)
if (!await context.Gateways.AnyAsync(o => o.Id == request.PaymentGatewayId, cancellationToken))
return Result.Fail("Gateway not found");
context.Ledger.Add(new Entities.PaymentLedger
{
CreatedAt = DateTime.UtcNow,
CustomerId = request.CustomerId,
OrderId = request.OrderId,
PaymentGatewayId = request.PaymentGatewayId,
PaymentGatewayReference = request.PaymentGatewayReference,
PaymentId = request.PaymentId,
Status = request.Status,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to create ledger entry");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<PaymentGateway>> GetPaymentGatewayAsync(long paymentGatewayId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var gateway = await context.Gateways.AsNoTracking().FirstOrDefaultAsync(g => g.Id == paymentGatewayId, cancellationToken);
return gateway is not null
? Result.Ok(gateway.ToModel())
: Result.Fail<PaymentGateway>("Could not find gateway");
}
catch (Exception ex)
{
return Result.Fail<PaymentGateway>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreatePaymentGatewayAsync(CreatePaymentGateway request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Gateways.AnyAsync(g => g.MerchantId == request.MerchantId && g.MerchantKey == request.MerchantKey, cancellationToken))
return Result.Fail<long>("A gateway with the same credentials already exists");
var gateway = context.Gateways.Add(new Entities.PaymentGateway
{
CreatedAt = DateTime.UtcNow,
Enabled = true,
IsSandbox = request.IsSandbox,
MerchantId = request.MerchantId,
MerchantKey = request.MerchantKey,
Name = request.Name,
Website = request.Website,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(gateway.Entity.Id)
: Result.Fail<long>("Failed to create payment gateway");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> CompletePaymentAsync(long paymentId, PaymentStatuses status, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (status == PaymentStatuses.NotPaid)
return Result.Fail("Cannot finalise a payment using NotPaid status");
var updatedRecords = await context.Payments
.Where(p => p.Id == paymentId && p.Status != PaymentStatuses.Paid && p.Status != status)
.ExecuteUpdateAsync(setters => setters
.SetProperty(u => u.Status, status)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
return updatedRecords > 0
? Result.Ok()
: Result.Fail("Failed to update payment");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdatePaymentAsync(long paymentId, decimal amount, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var updatedRecords = await context.Payments
.Where(p => p.Id == paymentId && p.Status == PaymentStatuses.NotPaid)
.ExecuteUpdateAsync(setters => setters
.SetProperty(u => u.Amount, amount)
.SetProperty(u => u.Status, PaymentStatuses.NotPaid)
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
return updatedRecords > 0
? Result.Ok()
: Result.Fail("Failed to update payment");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreatePaymentAsync(decimal amount, long orderId, string reference, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Payments.AnyAsync(p => p.OrderId == orderId && p.Amount == amount && p.Status != PaymentStatuses.Paid, cancellationToken))
return Result.Fail<long>("An order with the same amount already exists in the system");
var payment = context.Payments.Add(new Entities.Payment
{
CreatedAt = DateTime.UtcNow,
Amount = amount,
OrderId = orderId,
Reference = reference,
Status = PaymentStatuses.NotPaid,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(payment.Entity.Id)
: Result.Fail<long>("Failed to make payment");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
} }
@@ -1,6 +1,5 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities; using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
using LiteCharms.Features.MidrandBooks.Authors.Entities; using LiteCharms.Features.MidrandBooks.Authors.Entities;
using LiteCharms.Features.MidrandBooks.Categories.Entities;
using LiteCharms.Features.MidrandBooks.Customers.Entities; using LiteCharms.Features.MidrandBooks.Customers.Entities;
using LiteCharms.Features.MidrandBooks.Orders.Entities; using LiteCharms.Features.MidrandBooks.Orders.Entities;
using LiteCharms.Features.MidrandBooks.Pages.Entities; using LiteCharms.Features.MidrandBooks.Pages.Entities;
@@ -36,16 +35,4 @@ public sealed class MidrandBooksDbContext(DbContextOptions<MidrandBooksDbContext
public DbSet<Shipping> Shippings => Set<Shipping>(); public DbSet<Shipping> Shippings => Set<Shipping>();
public DbSet<ShippingProvider> ShippingProviders => Set<ShippingProvider>(); public DbSet<ShippingProvider> ShippingProviders => Set<ShippingProvider>();
public DbSet<Category> Categories => Set<Category>();
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
public DbSet<ProductInventory> Inventories => Set<ProductInventory>();
public DbSet<Payment> Payments => Set<Payment>();
public DbSet<PaymentGateway> Gateways => Set<PaymentGateway>();
public DbSet<PaymentLedger> Ledger => Set<PaymentLedger>();
} }
@@ -1,966 +0,0 @@
// <auto-generated />
using System;
using LiteCharms.Features.MidrandBooks.Postgres;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{
[DbContext(typeof(MidrandBooksDbContext))]
[Migration("20260530104851_AddedCategories")]
partial class AddedCategories
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("AuthorId")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<long>("ProductId")
.HasColumnType("bigint");
b.Property<int>("Ranking")
.HasColumnType("integer");
b.Property<int>("Rating")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("ProductId");
b.ToTable("Books");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Biography")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("ImageUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<int>("PublisherType")
.HasColumnType("integer");
b.Property<string>("ThumbnailImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("VatNumber")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Website")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.HasKey("Id");
b.ToTable("Authors", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<bool>("IsMain")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(15)
.HasColumnType("character varying(15)");
b.HasKey("Id");
b.ToTable("Categories", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<int>("BuildingType")
.HasColumnType("integer");
b.Property<string>("City")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Country")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("CustomerId")
.HasColumnType("bigint");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<bool>("IsPrimary")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PostalCode")
.IsRequired()
.HasColumnType("text");
b.Property<string>("State")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Street")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Addresses", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("CustomerId")
.HasColumnType("bigint");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<bool>("IsPrimary")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Contacts", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<string>("Company")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Phone")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("VatNumber")
.HasColumnType("text");
b.Property<string>("Website")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("CustomerId")
.HasColumnType("bigint");
b.Property<string>("InvoiceUrl")
.HasColumnType("text");
b.Property<string>("Notes")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<decimal>("Total")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.HasKey("Id");
b.ToTable("Orders", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("AuthorBookId")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("OrderId")
.HasColumnType("bigint");
b.Property<long>("ProductPriceId")
.HasColumnType("bigint");
b.Property<int>("Quantity")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("AuthorBookId");
b.HasIndex("OrderId");
b.HasIndex("ProductPriceId");
b.ToTable("OrderItems", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("AddressId")
.HasColumnType("bigint");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("OrderId")
.HasColumnType("bigint");
b.Property<long>("ShippingProviderId")
.HasColumnType("bigint");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<string>("TrackingNumber")
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.HasKey("Id");
b.HasIndex("AddressId");
b.HasIndex("OrderId")
.IsUnique();
b.HasIndex("ShippingProviderId");
b.ToTable("Shippings", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<string>("Name")
.HasColumnType("text");
b.Property<decimal?>("Price")
.HasColumnType("numeric");
b.Property<string>("TrackingUrl")
.HasColumnType("text");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("ShippingProviders");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("AuthorBookId")
.HasColumnType("bigint");
b.Property<byte[]>("Content")
.IsRequired()
.HasColumnType("bytea");
b.Property<int>("ContentType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.PrimitiveCollection<string[]>("Notes")
.HasColumnType("text[]");
b.Property<int>("Number")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.HasKey("Id");
b.HasIndex("AuthorBookId");
b.ToTable("BookPages", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("OrderId")
.HasColumnType("bigint");
b.Property<string>("Reason")
.HasMaxLength(1000)
.HasColumnType("character varying(1000)");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("Refunds", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.PrimitiveCollection<string[]>("Categories")
.HasColumnType("text[]");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("ImageUrl")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("character varying(255)");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string[]>("ThumbnailUrls")
.HasColumnType("text[]");
b.Property<int>("Type")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.HasKey("Id");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<long>("ProductId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.HasKey("Id");
b.HasIndex("ProductId");
b.ToTable("Prices", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author")
.WithMany("Books")
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Author");
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
{
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
{
b1.Property<long>("AuthorId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("ImageUrl");
b1.Property<string>("Name");
b1.Property<int>("Type");
b1.Property<string>("Url");
b1.HasKey("AuthorId", "__synthesizedOrdinal");
b1.ToTable("Authors");
b1
.ToJson("SocialMedia")
.HasColumnType("jsonb");
b1.WithOwner()
.HasForeignKey("AuthorId");
});
b.Navigation("SocialMedia");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
.WithMany("Addresses")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
.WithMany("Contacts")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
{
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
{
b1.Property<long>("CustomerId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("ImageUrl");
b1.Property<string>("Name");
b1.Property<int>("Type");
b1.Property<string>("Url");
b1.HasKey("CustomerId", "__synthesizedOrdinal");
b1.ToTable("Customers");
b1
.ToJson("SocialMedia")
.HasColumnType("jsonb");
b1.WithOwner()
.HasForeignKey("CustomerId");
});
b.Navigation("SocialMedia");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook")
.WithMany()
.HasForeignKey("AuthorBookId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
.WithMany("OrderItems")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("AuthorBook");
b.Navigation("Order");
b.Navigation("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address")
.WithMany()
.HasForeignKey("AddressId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
.WithOne("Shipping")
.HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider")
.WithMany("Shippings")
.HasForeignKey("ShippingProviderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Address");
b.Navigation("Order");
b.Navigation("ShippingProvider");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book")
.WithMany("Pages")
.HasForeignKey("AuthorBookId")
.OnDelete(DeleteBehavior.NoAction)
.IsRequired();
b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 =>
{
b1.Property<long>("BookPageId");
b1.Property<int>("__synthesizedOrdinal")
.ValueGeneratedOnAdd();
b1.Property<string>("Description");
b1.Property<string>("Tag");
b1.Property<string>("Url");
b1.HasKey("BookPageId", "__synthesizedOrdinal");
b1.ToTable("BookPages");
b1
.ToJson("References")
.HasColumnType("jsonb");
b1.WithOwner()
.HasForeignKey("BookPageId");
});
b.Navigation("Book");
b.Navigation("References");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
.WithMany("Refunds")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{
b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 =>
{
b1.Property<long>("ProductId");
b1.Property<string>("CopyrightInfo");
b1.Property<string>("ManufactureDate");
b1.Property<string>("Manufacturer");
b1.Property<string>("SerialNumber");
b1.HasKey("ProductId");
b1.ToTable("Products");
b1
.ToJson("Metadata")
.HasColumnType("jsonb");
b1.WithOwner()
.HasForeignKey("ProductId");
});
b.Navigation("Metadata");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
.WithMany("Prices")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{
b.Navigation("Pages");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
{
b.Navigation("Books");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
{
b.Navigation("Addresses");
b.Navigation("Contacts");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b =>
{
b.Navigation("OrderItems");
b.Navigation("Refunds");
b.Navigation("Shipping");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b =>
{
b.Navigation("Shippings");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{
b.Navigation("Prices");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,37 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddedCategories : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "character varying(15)", maxLength: 15, nullable: false),
IsMain = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Categories");
}
}
}
@@ -1,68 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddedProductCategories : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Categories",
table: "Products");
migrationBuilder.CreateTable(
name: "ProductCategories",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
ProductId = table.Column<long>(type: "bigint", nullable: false),
CategoryId = table.Column<long>(type: "bigint", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProductCategories", x => x.Id);
table.ForeignKey(
name: "FK_ProductCategories_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ProductCategories_Products_ProductId",
column: x => x.ProductId,
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_ProductCategories_CategoryId",
table: "ProductCategories",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_ProductCategories_ProductId",
table: "ProductCategories",
column: "ProductId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ProductCategories");
migrationBuilder.AddColumn<string[]>(
name: "Categories",
table: "Products",
type: "text[]",
nullable: true);
}
}
}
@@ -1,185 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddedPaymentObjects : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Gateways",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Name = table.Column<string>(type: "text", nullable: false),
Website = table.Column<string>(type: "text", nullable: true),
MerchantId = table.Column<string>(type: "text", nullable: false),
MerchantKey = table.Column<string>(type: "text", nullable: false),
Passphrase = table.Column<string>(type: "text", nullable: false),
IsSandbox = table.Column<bool>(type: "boolean", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Gateways", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Inventories",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
Status = table.Column<int>(type: "integer", nullable: false),
ProductId = table.Column<long>(type: "bigint", nullable: false),
ProductPriceId = table.Column<long>(type: "bigint", nullable: false),
TotalAllocated = table.Column<int>(type: "integer", nullable: false),
TotalReserved = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Inventories", x => x.Id);
table.ForeignKey(
name: "FK_Inventories_Prices_ProductPriceId",
column: x => x.ProductPriceId,
principalTable: "Prices",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Inventories_Products_ProductId",
column: x => x.ProductId,
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Payments",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
OrderId = table.Column<long>(type: "bigint", nullable: false),
Reference = table.Column<string>(type: "text", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Payments", x => x.Id);
table.ForeignKey(
name: "FK_Payments_Orders_OrderId",
column: x => x.OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Ledger",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
Status = table.Column<int>(type: "integer", nullable: false),
OrderId = table.Column<long>(type: "bigint", nullable: false),
PaymentId = table.Column<long>(type: "bigint", nullable: false),
CustomerId = table.Column<long>(type: "bigint", nullable: false),
PaymentGatewayReference = table.Column<string>(type: "text", nullable: true),
PaymentGatewayId = table.Column<long>(type: "bigint", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Ledger", x => x.Id);
table.ForeignKey(
name: "FK_Ledger_Customers_CustomerId",
column: x => x.CustomerId,
principalTable: "Customers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Ledger_Gateways_PaymentGatewayId",
column: x => x.PaymentGatewayId,
principalTable: "Gateways",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Ledger_Orders_OrderId",
column: x => x.OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Ledger_Payments_PaymentId",
column: x => x.PaymentId,
principalTable: "Payments",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Inventories_ProductId",
table: "Inventories",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_Inventories_ProductPriceId",
table: "Inventories",
column: "ProductPriceId");
migrationBuilder.CreateIndex(
name: "IX_Ledger_CustomerId",
table: "Ledger",
column: "CustomerId");
migrationBuilder.CreateIndex(
name: "IX_Ledger_OrderId",
table: "Ledger",
column: "OrderId");
migrationBuilder.CreateIndex(
name: "IX_Ledger_PaymentGatewayId",
table: "Ledger",
column: "PaymentGatewayId");
migrationBuilder.CreateIndex(
name: "IX_Ledger_PaymentId",
table: "Ledger",
column: "PaymentId");
migrationBuilder.CreateIndex(
name: "IX_Payments_OrderId",
table: "Payments",
column: "OrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Inventories");
migrationBuilder.DropTable(
name: "Ledger");
migrationBuilder.DropTable(
name: "Gateways");
migrationBuilder.DropTable(
name: "Payments");
}
}
}
@@ -1,29 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{
/// <inheritdoc />
public partial class RemovedPassphraseFromPaymentGateway : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Passphrase",
table: "Gateways");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Passphrase",
table: "Gateways",
type: "text",
nullable: false,
defaultValue: "");
}
}
}
@@ -130,34 +130,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.ToTable("Authors", (string)null); b.ToTable("Authors", (string)null);
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<bool>("Enabled")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<bool>("IsMain")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(15)
.HasColumnType("character varying(15)");
b.HasKey("Id");
b.ToTable("Categories", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
@@ -536,129 +508,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.ToTable("BookPages", (string)null); b.ToTable("BookPages", (string)null);
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("OrderId")
.HasColumnType("bigint");
b.Property<string>("Reference")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("Payments", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<bool>("IsSandbox")
.HasColumnType("boolean");
b.Property<string>("MerchantId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("MerchantKey")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Gateways", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("CustomerId")
.HasColumnType("bigint");
b.Property<long>("OrderId")
.HasColumnType("bigint");
b.Property<long?>("PaymentGatewayId")
.HasColumnType("bigint");
b.Property<string>("PaymentGatewayReference")
.HasColumnType("text");
b.Property<long>("PaymentId")
.HasColumnType("bigint");
b.Property<int>("Status")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId");
b.HasIndex("PaymentGatewayId");
b.HasIndex("PaymentId");
b.ToTable("Ledger", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
@@ -709,6 +558,9 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.PrimitiveCollection<string[]>("Categories")
.HasColumnType("text[]");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -753,66 +605,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.ToTable("Products", (string)null); b.ToTable("Products", (string)null);
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<long>("CategoryId")
.HasColumnType("bigint");
b.Property<long>("ProductId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.HasIndex("ProductId");
b.ToTable("ProductCategories", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<long>("ProductId")
.HasColumnType("bigint");
b.Property<long>("ProductPriceId")
.HasColumnType("bigint");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<int>("TotalAllocated")
.HasColumnType("integer");
b.Property<int>("TotalReserved")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ProductId");
b.HasIndex("ProductPriceId");
b.ToTable("Inventories", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
{ {
b.Property<long>("Id") b.Property<long>("Id")
@@ -1051,51 +843,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("References"); b.Navigation("References");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
.WithMany()
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway")
.WithMany()
.HasForeignKey("PaymentGatewayId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment")
.WithMany()
.HasForeignKey("PaymentId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Customer");
b.Navigation("Gateway");
b.Navigation("Order");
b.Navigation("Payment");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order") b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
@@ -1136,44 +883,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Metadata"); b.Navigation("Metadata");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductCategory", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
.WithMany("Categories")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Price");
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
@@ -1218,8 +927,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{ {
b.Navigation("Categories");
b.Navigation("Prices"); b.Navigation("Prices");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
@@ -3,7 +3,5 @@
[EntityTypeConfiguration<ProductConfiguration, Product>] [EntityTypeConfiguration<ProductConfiguration, Product>]
public class Product : Models.Product public class Product : Models.Product
{ {
public virtual ICollection<ProductCategory> Categories { get; set; } = [];
public virtual ICollection<ProductPrice> Prices { get; set; } = []; public virtual ICollection<ProductPrice> Prices { get; set; } = [];
} }
@@ -1,11 +0,0 @@
using LiteCharms.Features.MidrandBooks.Categories.Entities;
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
[EntityTypeConfiguration<ProductCategoryConfiguration, ProductCategory>]
public class ProductCategory : Models.ProductCategory
{
public virtual Product? Product { get; set; }
public virtual Category? Category { get; set; }
}
@@ -1,23 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
public sealed class ProductCategoryConfiguration : IEntityTypeConfiguration<ProductCategory>
{
public void Configure(EntityTypeBuilder<ProductCategory> builder)
{
builder.ToTable("ProductCategories");
builder.HasKey(p => p.Id);
builder.Property(p => p.ProductId).IsRequired();
builder.Property(p => p.CategoryId).IsRequired();
builder.HasOne(p => p.Product)
.WithMany(p => p.Categories)
.HasForeignKey(p => p.ProductId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(c => c.Category)
.WithMany()
.HasForeignKey(c => c.CategoryId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -17,6 +17,7 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
builder.Property(f => f.Description).HasMaxLength(1024); builder.Property(f => f.Description).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).HasMaxLength(1024); builder.Property(f => f.ImageUrl).HasMaxLength(1024);
builder.Property(f => f.Enabled).HasDefaultValue(false); builder.Property(f => f.Enabled).HasDefaultValue(false);
builder.Property(f => f.Categories).IsRequired(false);
builder.Property(f => f.ThumbnailUrls).IsRequired(false); builder.Property(f => f.ThumbnailUrls).IsRequired(false);
builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); }); builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); });
@@ -1,9 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
[EntityTypeConfiguration<ProductInventoryConfiguration, ProductInventory>]
public class ProductInventory : Models.ProductInventory
{
public virtual Product? Product { get; set; }
public virtual ProductPrice? Price { get; set; }
}
@@ -1,27 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
public sealed class ProductInventoryConfiguration : IEntityTypeConfiguration<ProductInventory>
{
public void Configure(EntityTypeBuilder<ProductInventory> builder)
{
builder.ToTable("Inventories");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.Status).IsRequired();
builder.Property(f => f.TotalAllocated).IsRequired();
builder.Property(f => f.TotalReserved).IsRequired();
builder.Property(f => f.ProductId).IsRequired();
builder.Property(f => f.ProductPriceId).IsRequired();
builder.HasOne(f => f.Product)
.WithMany()
.HasForeignKey(f => f.ProductId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne(f => f.Price)
.WithMany()
.HasForeignKey(f => f.ProductPriceId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -22,6 +22,8 @@ public class Product
public string[]? ThumbnailUrls { get; set; } public string[]? ThumbnailUrls { get; set; }
public string[]? Categories { get; set; }
public ProductMetadata? Metadata { get; set; } public ProductMetadata? Metadata { get; set; }
public ProductPrice? Price { get; set; } public ProductPrice? Price { get; set; }
@@ -1,10 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public class ProductCategory
{
public long Id { get; set; }
public long ProductId { get; set; }
public long CategoryId { get; set; }
}
@@ -1,18 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public class ProductInventory
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public InventoryStatuses Status { get; set; }
public long ProductId { get; set; }
public long ProductPriceId { get; set; }
public int TotalAllocated { get; set; }
public int TotalReserved { get; set; }
}
@@ -2,24 +2,6 @@
namespace LiteCharms.Features.MidrandBooks.Products.Models; namespace LiteCharms.Features.MidrandBooks.Products.Models;
public sealed record ReserveStock
{
public required long ProductId { get; set; }
public required long ProductPriceId { get; set; }
public int Reservation { get; set; }
}
public sealed record AllocateStock
{
public required long ProductId { get; set; }
public required long ProductPriceId { get; set; }
public int Allocation { get; set; }
}
public sealed record CreateProduct public sealed record CreateProduct
{ {
public required ProductTypes Type { get; set; } public required ProductTypes Type { get; set; }
@@ -1,226 +1,13 @@
using LiteCharms.Features.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Categories.Models;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.MidrandBooks.Products.Models; using LiteCharms.Features.MidrandBooks.Products.Models;
using LiteCharms.Features.Models; using LiteCharms.Features.Models;
using Org.BouncyCastle.Asn1.Ocsp;
namespace LiteCharms.Features.MidrandBooks.Products; namespace LiteCharms.Features.MidrandBooks.Products;
public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{ {
public async ValueTask<Result<ProductInventory>> CheckProductStockAvailabilityAsync(long productId, long productPriceId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var inventory = await context.Inventories
.AsNoTracking()
.Where(i => i.ProductPriceId == productPriceId && i.ProductId == productId)
.OrderByDescending(o => o.Id)
.FirstOrDefaultAsync(cancellationToken);
return inventory is not null
? Result.Ok(inventory.ToModel())
: Result.Fail<ProductInventory>("Product sold out");
}
catch (Exception ex)
{
return Result.Fail<ProductInventory>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> ReserveProductInventoryAsync(ReserveStock request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var oldInventory = await context.Inventories
.AsNoTracking()
.Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId)
.OrderByDescending(o => o.Id)
.FirstOrDefaultAsync(cancellationToken);
var newAllocation = 0;
var newReservation = 0;
if (oldInventory is not null)
{
newAllocation = oldInventory.TotalAllocated;
newReservation = oldInventory.TotalReserved + request.Reservation;
}
else
{
newAllocation = 0;
newReservation = request.Reservation;
}
if (newAllocation - newReservation < 0)
return Result.Fail<long>("Allocation failure: The requested book quantity exceeds current physical inventory availability.");
var inventory = context.Inventories.Add(new Entities.ProductInventory
{
CreatedAt = DateTime.UtcNow,
ProductId = request.ProductId,
ProductPriceId = request.ProductPriceId,
Status = InventoryStatuses.Reserved,
TotalAllocated = newAllocation,
TotalReserved = newReservation,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(inventory.Entity.Id)
: Result.Fail<long>("Failed to create inventory entry");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> AllocateProductInventoryAsync(AllocateStock request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var oldInventory = await context.Inventories
.AsNoTracking()
.Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId)
.OrderByDescending(o => o.Id)
.FirstOrDefaultAsync(cancellationToken);
var newAllocation = 0;
var newReservation = 0;
if (oldInventory is not null)
{
newAllocation = oldInventory.TotalAllocated + request.Allocation;
newReservation = oldInventory.TotalReserved;
}
else
{
newAllocation = request.Allocation;
newReservation = 0;
}
var inventory = context.Inventories.Add(new Entities.ProductInventory
{
CreatedAt = DateTime.UtcNow,
ProductId = request.ProductId,
ProductPriceId = request.ProductPriceId,
Status = InventoryStatuses.Adjustment,
TotalAllocated = newAllocation,
TotalReserved = newReservation,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(inventory.Entity.Id)
: Result.Fail<long>("Failed to create inventory entry");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Products.AnyAsync(p => p.Id == productId && p.Enabled, cancellationToken))
return Result.Fail("Product does not exist");
if (!await context.Categories.AnyAsync(c => c.Id == categoryId && c.Enabled, cancellationToken))
return Result.Fail("Category does not exist");
if (await context.ProductCategories.AnyAsync(c => c.ProductId == productId && c.CategoryId == categoryId, cancellationToken))
return Result.Fail("Category already assigned to product");
context.ProductCategories.Add(new Entities.ProductCategory
{
ProductId = productId,
CategoryId = categoryId,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Could not add category to product");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Category[]>> GetProductCategoriesAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var categories = await context.ProductCategories.AsNoTracking()
.Where(p => p.ProductId == productId)
.OrderByDescending(o => o.Id)
.Select(p => p.Category)
.ToArrayAsync(cancellationToken);
return categories?.Length > 0
? Result.Ok(categories.Select(c => c!.ToModel()).ToArray())
: Result.Fail<Category[]>("Failed to get product categories");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> DeleteProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rowsDeleted = await context.ProductCategories
.Where(p => p.ProductId == productId && p.CategoryId == categoryId)
.ExecuteDeleteAsync(cancellationToken);
return rowsDeleted > 0
? Result.Ok()
: Result.Fail("No product categories were deleted");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> DeleteAllProductCategoriesAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rowsDeleted = await context.ProductCategories
.Where(p => p.ProductId == productId)
.ExecuteDeleteAsync(cancellationToken);
return rowsDeleted > 0
? Result.Ok()
: Result.Fail("No product categories were deleted");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default) public async ValueTask<Result> UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default)
{ {
try try
@@ -281,6 +68,9 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
if (!string.IsNullOrWhiteSpace(filter.Title)) if (!string.IsNullOrWhiteSpace(filter.Title))
query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%")); query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%"));
if (!string.IsNullOrWhiteSpace(filter.Category))
query = query.Where(p => p.Categories.Contains(filter.Category));
if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) if (!string.IsNullOrWhiteSpace(filter.Manufacturer))
query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%")); query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%"));
@@ -362,6 +152,7 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
ImageUrl = request.ImageUrl, ImageUrl = request.ImageUrl,
ThumbnailUrls = request.ThumbnailUrls, ThumbnailUrls = request.ThumbnailUrls,
Metadata = request.Metadata, Metadata = request.Metadata,
Categories = request.Categories,
Enabled = true Enabled = true
}); });
@@ -422,8 +213,7 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products var product = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
return product is null return product is null
? Result.Fail<Product>(new Error($"Product with ID {productId} not found.")) ? Result.Fail<Product>(new Error($"Product with ID {productId} not found."))
@@ -1,8 +1,9 @@
using LiteCharms.Features.Hasher; using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models; using LiteCharms.Features.Models;
using LiteCharms.Features.TechShop.Extensions; using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.TechShop.Leads.Models; using LiteCharms.Features.TechShop.Leads.Models;
using LiteCharms.Features.TechShop.Postgres; using LiteCharms.Features.TechShop.Postgres;
using static LiteCharms.Features.Extensions.Hash;
namespace LiteCharms.Features.TechShop.Leads; namespace LiteCharms.Features.TechShop.Leads;
@@ -28,7 +29,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
FeedItemId = request.FeedItemId, FeedItemId = request.FeedItemId,
Status = LeadStatus.New, Status = LeadStatus.New,
TargetId = request.TargetId, TargetId = request.TargetId,
AttributionHash = HashService.StringToSha256Hash($"{request.ClickId}{request.AppClickId}{request.WebClickId}") AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
-1
View File
@@ -26,7 +26,6 @@ public class Fixture : IDisposable
.AddGarageS3(Configuration) .AddGarageS3(Configuration)
.AddEmailServices(Configuration) .AddEmailServices(Configuration)
.AddSingleton(Configuration) .AddSingleton(Configuration)
.AddHashServices(Configuration)
.BuildServiceProvider(); .BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>(); Mediator = Services.GetRequiredService<IMediator>();
@@ -1,155 +0,0 @@
using LiteCharms.Features.Hasher;
using LiteCharms.Features.Models;
using static LiteCharms.Features.Extensions.Hash;
namespace LiteCharms.Features.Tests;
public class HashServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly HashService hashService = fixture.Services.GetRequiredService<HashService>();
private readonly string payfastPassphrase = fixture.Configuration.GetSection("HasherSettings:PayfastPassphrase").Value!;
[Fact]
public void StringToSha256Hash_Should_GenerateHash()
{
var input = "We are the best";
var expectedHash = "96E17275B53F6BEB7A0D1C4F789F226D3C71CBE398585F25B3028F2B432E78AB";
var result = HashService.StringToSha256Hash(input);
Assert.NotNull(result);
Assert.True(HashService.IsSha256Hash(result));
Assert.Equal(expectedHash, result);
}
[Fact]
public void StreamToSha256Hash_Should_GenerateHash()
{
var input = "We are successful";
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(input));
var expectedHash = "C27872EE494B09D72203C98FC858268F3CD3492D62AA7B766A873520C2C73AFB";
var result = HashService.StreamToSha256Hash(stream);
Assert.NotNull(result);
Assert.True(HashService.IsSha256Hash(result));
Assert.Equal(expectedHash, result);
}
[Fact]
public void BytesToSha256Hash_Should_GenerateHash()
{
var inputBytes = Encoding.UTF8.GetBytes("We are wealthy");
var expectedHash = "3876BF98F6E4A8E42B22C40415687D6FF13F0E887F3F508B71852298FC665737";
var result = HashService.BytesToSha256Hash(inputBytes);
Assert.NotNull(result);
Assert.True(HashService.IsSha256Hash(result));
Assert.Equal(expectedHash, result);
}
[Fact]
public void ToMd5Hash_Should_GenerateHash()
{
var input = "We manifest our desired destiny";
var expectedMd5Lowercase = "6c7816869bcebe4634f7afe9c66dfa08";
var result = HashService.ToMd5Hash(input);
Assert.True(result.IsSuccess);
Assert.True(HashService.IsMd5Hash(result.Value));
Assert.Equal(expectedMd5Lowercase, result.Value);
}
[Fact]
public void VerifyPayfastWebhookSignature_Should_GenerateHash()
{
var paymentId = hashService.HashEncodeLongId(1001).Value;
var payload = new PayfastWebhookPayload
{
Amount = "350.00",
ItemName = "System Architecture Book",
MPaymentId = paymentId,
};
var rawPayload = payload.ToRawPayfastPayload(payfastPassphrase);
var generatedSignature = HashService.ToMd5Hash(rawPayload).Value;
var result = hashService.VerifyPayfastWebhookSignature(payload, generatedSignature);
Assert.True(result.IsSuccess);
Assert.True(result.Value);
}
[Fact]
public void HashEncodeHex_Should_GenerateHash()
{
var validHexInput = "DEADBEEF42";
var result = hashService.HashEncodeHex(validHexInput);
Assert.True(result.IsSuccess);
Assert.False(string.IsNullOrWhiteSpace(result.Value));
}
[Fact]
public void HashEncodeIntId_Should_GenerateHash()
{
int targetId = 42;
var result = hashService.HashEncodeIntId(targetId);
Assert.True(result.IsSuccess);
Assert.True(result.Value.Length >= 10);
}
[Fact]
public void HashEncodeLongId_Should_GenerateHash()
{
long targetId = 9904185012L;
var result = hashService.HashEncodeLongId(targetId);
Assert.True(result.IsSuccess);
Assert.True(result.Value.Length >= 10);
}
[Fact]
public void DecodeIntIdHash_Should_GenerateHash()
{
int originalId = 88041;
var hashedString = hashService.HashEncodeIntId(originalId).Value;
var result = hashService.DecodeIntIdHash(hashedString);
Assert.True(result.IsSuccess);
Assert.Equal(originalId, result.Value);
}
[Fact]
public void DecodeLongIdHash_Should_GenerateHash()
{
long originalId = 9081230491823L;
var hashedString = hashService.HashEncodeLongId(originalId).Value;
var result = hashService.DecodeLongIdHash(hashedString);
Assert.True(result.IsSuccess);
Assert.Equal(originalId, result.Value);
}
[Fact]
public void DecodeHexHash_Should_GenerateHash()
{
var originalHex = "ABCDEF12345";
var hashedString = hashService.HashEncodeHex(originalHex).Value;
var result = hashService.DecodeHexHash(hashedString);
Assert.True(result.IsSuccess);
Assert.Equal(originalHex, result.Value);
}
}
@@ -27,8 +27,6 @@
<!-- Global Usings --> <!-- Global Usings -->
<ItemGroup> <ItemGroup>
<Using Include="System.Net" />
<Using Include="System.Text" />
<Using Include="Mediator" /> <Using Include="Mediator" />
<Using Include="Xunit.Abstractions" /> <Using Include="Xunit.Abstractions" />
<Using Include="Microsoft.Extensions.DependencyInjection" /> <Using Include="Microsoft.Extensions.DependencyInjection" />
@@ -1,3 +0,0 @@
namespace LiteCharms.Features.Abstractions;
public interface IService;
+1 -93
View File
@@ -1,35 +1,5 @@
namespace LiteCharms.Features; namespace LiteCharms.Features;
public enum InventoryStatuses : int
{
Adjustment = 0,
Reserved = 1,
Released = 2,
Sold = 3,
Replenished = 4,
Correction = 5,
}
public enum LedgerStatuses : int
{
Changed = 0,
Sent = 1,
Received = 2,
Refunded = 3,
Cancelled = 4,
Failed = 5,
Partial = 6,
}
public enum PaymentStatuses : int
{
NotPaid = 0,
Paid = 1,
Cancelled = 2,
Requested = 3,
Failed = 4,
}
public enum ShippingProviderTypes : int public enum ShippingProviderTypes : int
{ {
Dsv = 0, Dsv = 0,
@@ -123,8 +93,7 @@ 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
@@ -145,64 +114,3 @@ public enum Priorities : int
Medium = 1, Medium = 1,
High = 2, High = 2,
} }
public enum PublisherTypes : int
{
Individual = 0,
Company = 1,
Organization = 2,
SelfPublished = 3,
UniversityPress = 4,
GovernmentAgency = 5,
NonProfit = 6,
Independent = 7
}
public enum BookTypes : int
{
Fiction = 0,
NonFiction = 1,
Academic = 2,
SelfHelp = 3,
Biography = 4,
Poetry = 5,
Children = 6,
YoungAdult = 7,
ScienceFiction = 8,
Fantasy = 9
}
public enum BookContentTypes : int
{
Text = 0,
Image = 1,
Video = 2,
Audio = 3,
Interactive = 4,
Markdown = 5,
Html = 6,
Json = 7,
Yaml = 8
}
public enum BookPageTypes : int
{
Cover = 0,
Preface = 1,
Introduction = 2,
Content = 3,
Closing = 4,
Referencer = 5,
Credits = 6,
BackCover = 7
}
public enum ProductTypes : int
{
Book = 1,
Journal = 2,
Magazine = 3,
EBook = 4,
Audiobook = 5,
Accessory = 6
}
+7 -96
View File
@@ -1,102 +1,13 @@
using LiteCharms.Features.Hasher; namespace LiteCharms.Features.Extensions;
using LiteCharms.Features.Hasher.Configuration;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.Extensions;
public static class Hash public static class Hash
{ {
public const string HasherConfigSectionName = "HasherSettings"; public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
public static IServiceCollection AddHashServices(this IServiceCollection services, IConfiguration configuration) public static readonly Func<Stream, string?> StreamToSha256Hash = (stream) =>
{ Convert.ToHexString(SHA256.HashData(stream));
services.Configure<HasherSettings>(configuration.GetSection(HasherConfigSectionName));
var settings = configuration.GetSection(HasherConfigSectionName).Get<HasherSettings>(); public static readonly Func<byte[], string?> BytesToSha256Hash = (bytes) =>
Convert.ToHexString(SHA256.HashData(bytes));
services.AddSingleton<IHashids>(_ =>
new Hashids(settings!.Salt, minHashLength: settings.MinHashLength));
services.AddSingleton<HashService>();
return services;
}
public static string ToRawPayfastPayload(this PayfastWebhookPayload input, string passphrase)
{
var parameters = new List<string>();
if (!string.IsNullOrWhiteSpace(input.Amount))
parameters.Add($"amount={WebUtility.UrlEncode(input.Amount)}");
if (!string.IsNullOrWhiteSpace(input.ItemName))
parameters.Add($"item_name={WebUtility.UrlEncode(input.ItemName)}");
if (!string.IsNullOrWhiteSpace(input.MPaymentId))
parameters.Add($"m_payment_id={WebUtility.UrlEncode(input.MPaymentId)}");
string payload = string.Join("&", parameters);
if (!string.IsNullOrWhiteSpace(passphrase))
payload += $"&passphrase={WebUtility.UrlEncode(passphrase)}";
return payload;
}
public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this string rawPayload)
{
string passphrase = string.Empty;
var payload = new PayfastWebhookPayload();
if (string.IsNullOrWhiteSpace(rawPayload)) return (payload, passphrase);
var segments = rawPayload.Split('&', StringSplitOptions.RemoveEmptyEntries);
foreach (var segment in segments)
{
int delimiterIndex = segment.IndexOf('=');
if (delimiterIndex == -1)
continue;
string key = segment[..delimiterIndex].Trim();
string rawValue = segment[(delimiterIndex + 1)..];
string decodedValue = WebUtility.UrlDecode(rawValue);
switch (key.ToLowerInvariant())
{
case "amount":
payload.Amount = decodedValue;
break;
case "item_name":
payload.ItemName = decodedValue;
break;
case "m_payment_id":
payload.MPaymentId = decodedValue;
break;
case "passphrase":
passphrase = decodedValue;
break;
}
}
return (payload, passphrase);
}
public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this IFormCollection form)
{
string passphrase = string.Empty;
var payload = new PayfastWebhookPayload();
if (form.IsNullOrEmpty()) return (payload, passphrase);
payload = new PayfastWebhookPayload
{
Amount = form.TryGetValue("amount", out var amountValues) ? amountValues.ToString() : null,
ItemName = form.TryGetValue("item_name", out var itemValues) ? itemValues.ToString() : null,
MPaymentId = form.TryGetValue("m_payment_id", out var paymentIdValues) ? paymentIdValues.ToString() : null,
};
return (payload, passphrase);
}
} }
@@ -1,10 +0,0 @@
namespace LiteCharms.Features.Hasher.Configuration;
public sealed class HasherSettings
{
public string? Salt { get; set; }
public int MinHashLength { get; set; }
public string? PayfastPassphrase { get; set; }
}
-131
View File
@@ -1,131 +0,0 @@
using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Hasher.Configuration;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.Hasher;
public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings> options) : IService
{
private readonly HasherSettings settings = options.Value;
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
private static partial Regex HexHashRegex { get; }
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
private static partial Regex Md5Regex { get; }
[GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
private static partial Regex Sha256Regex { get; }
public static bool IsMd5Hash(string? value) =>
!string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value);
public static bool IsSha256Hash(string? value) =>
!string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value);
public static string? StringToSha256Hash(string? input) =>
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
public static string? StreamToSha256Hash(Stream stream) =>
stream is null ? null : Convert.ToHexString(SHA256.HashData(stream));
public static string? BytesToSha256Hash(byte[] bytes) =>
bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes));
public static Result<string> ToMd5Hash(string input)
{
if (string.IsNullOrEmpty(input))
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
}
public Result<bool> VerifyPayfastWebhookSignature(PayfastWebhookPayload payload, string incomingSignature)
{
try
{
if (string.IsNullOrWhiteSpace(incomingSignature))
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
var parameters = new List<string>();
if (!string.IsNullOrWhiteSpace(payload.Amount))
parameters.Add($"amount={WebUtility.UrlEncode(payload.Amount)}");
if (!string.IsNullOrWhiteSpace(payload.ItemName))
parameters.Add($"item_name={WebUtility.UrlEncode(payload.ItemName)}");
if (!string.IsNullOrWhiteSpace(payload.MPaymentId))
parameters.Add($"m_payment_id={WebUtility.UrlEncode(payload.MPaymentId)}");
string signatureString = string.Join("&", parameters);
if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase))
signatureString += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}";
var localHashResult = ToMd5Hash(signatureString);
if (!localHashResult.IsSuccess)
return Result.Fail<bool>(localHashResult.Errors);
bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase);
return Result.Ok(isValid);
}
catch (Exception ex)
{
return Result.Fail<bool>(new Error("An error occurred during Payfast MD5 verification.").CausedBy(ex));
}
}
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
? Result.Fail<string>("Input must be a valid hexadecimal string.")
: Result.Ok(hasher.EncodeHex(input));
public Result<string> HashEncodeIntId(int id) => id < 0
? Result.Fail<string>("Id cannot be negative.")
: Result.Ok(hasher.Encode(id));
public Result<string> HashEncodeLongId(long id) => id < 0
? Result.Fail<string>("Id cannot be negative.")
: Result.Ok(hasher.EncodeLong(id));
public Result<int> DecodeIntIdHash(string hash)
{
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<int>("Invalid token layout.");
int[] decoded = hasher.Decode(hash);
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<int>("Invalid or modified Int hash token.");
}
public Result<long> DecodeLongIdHash(string hash)
{
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<long>("Invalid token layout.");
long[] decoded = hasher.DecodeLong(hash);
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<long>("Invalid or modified Long hash token.");
}
public Result<string> DecodeHexHash(string hex)
{
try
{
string decoded = hasher.DecodeHex(hex);
return string.IsNullOrEmpty(decoded)
? Result.Fail<string>("Invalid or corrupted hex hash.")
: Result.Ok(decoded);
}
catch (FormatException fex)
{
return Result.Fail<string>(new Error("Invalid hash structure.").CausedBy(fex));
}
catch (Exception ex)
{
return Result.Fail<string>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -31,7 +31,6 @@
<!-- Quartz Scheduler--> <!-- Quartz Scheduler-->
<ItemGroup> <ItemGroup>
<PackageReference Include="Hashids.net" Version="1.7.0" />
<PackageReference Include="Meziantou.Analyzer" Version="3.0.96"> <PackageReference Include="Meziantou.Analyzer" Version="3.0.96">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -147,11 +146,6 @@
<!-- Shared Usings --> <!-- Shared Usings -->
<ItemGroup> <ItemGroup>
<Using Include="Microsoft.IdentityModel.Tokens" />
<Using Include="Microsoft.AspNetCore.Http" />
<Using Include="HashidsNet" />
<Using Include="System.Net" />
<Using Include="System.Text.RegularExpressions" />
<Using Include="System.Globalization" /> <Using Include="System.Globalization" />
<Using Include="Microsoft.AspNetCore.Builder" /> <Using Include="Microsoft.AspNetCore.Builder" />
<Using Include="Microsoft.Extensions.Hosting" /> <Using Include="Microsoft.Extensions.Hosting" />
@@ -1,8 +0,0 @@
namespace LiteCharms.Features.Models;
public sealed class PayfastWebhookPayload
{
public string? Amount { get; set; }
public string? ItemName { get; set; }
public string? MPaymentId { get; set; }
}
@@ -6,6 +6,8 @@ public sealed class ProductFilter
public string? Title { get; set; } public string? Title { get; set; }
public string? Category { get; set; }
public string? Manufacturer { get; set; } public string? Manufacturer { get; set; }
public string? SerialNumber { get; set; } public string? SerialNumber { get; set; }
@@ -1,4 +1,4 @@
using LiteCharms.Features.Hasher; using static LiteCharms.Features.Extensions.Hash;
namespace LiteCharms.Features.S3.Abstractions; namespace LiteCharms.Features.S3.Abstractions;
@@ -26,7 +26,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);
var fileHash = HashService.StreamToSha256Hash(stream); var fileHash = StreamToSha256Hash(stream);
if(string.IsNullOrWhiteSpace(fileHash)) if(string.IsNullOrWhiteSpace(fileHash))
return Result.Fail<string>("Failed to compute file hash."); return Result.Fail<string>("Failed to compute file hash.");
@@ -39,7 +39,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
Key = fileKey, Key = fileKey,
InputStream = stream, InputStream = stream,
ContentType = contentType, ContentType = contentType,
UseChunkEncoding = false, UseChunkEncoding = false
}; };
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);
-1
View File
@@ -9,7 +9,6 @@
<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" />