Compare commits

...

55 Commits

Author SHA1 Message Date
khwezi 0ab14d8b63 Merge pull request 'midrandshop' (#46) from midrandshop into master
Reviewed-on: #46
2026-05-30 00:14:10 +02:00
Khwezi Mngoma 2db3b3d293 Added customer seeder with order data
continuous-integration/drone/pr Build is passing
2026-05-30 00:11:19 +02:00
Khwezi Mngoma 50eee03dbe Product seeding completed 2026-05-29 23:02:06 +02:00
khwezi 466458e230 Merge pull request 'Implemented Product Data Seeder' (#45) from midrandshop into master
Reviewed-on: #45
2026-05-29 18:56:45 +02:00
Khwezi Mngoma 60fcc70e98 Implemented Product Data Seeder
continuous-integration/drone/pr Build is passing
2026-05-29 18:56:08 +02:00
khwezi 141d32f591 Merge pull request 'Refactored migrations' (#44) from midrandshop into master
Reviewed-on: #44
2026-05-29 09:03:27 +02:00
Khwezi Mngoma 38e765203d Refactored migrations
continuous-integration/drone/pr Build is passing
2026-05-29 09:02:47 +02:00
khwezi d9e7f225ae Merge pull request 'midrandshop' (#43) from midrandshop into master
Reviewed-on: #43
2026-05-29 08:23:15 +02:00
Khwezi Mngoma 9eb3526a2e Moved Book Page DTOs
continuous-integration/drone/pr Build is passing
2026-05-29 08:21:11 +02:00
Khwezi Mngoma 4397976ed8 Implemented order service tests 2026-05-29 08:18:29 +02:00
Khwezi Mngoma 2546c34ffc Wrote tests for most services, applied EF core optimisations 2026-05-29 01:05:22 +02:00
Khwezi Mngoma 4e53ff8a37 ProductService tested and stable 2026-05-28 17:28:33 +02:00
Khwezi Mngoma 2a0b34c730 Refactored database registration to allow postgres to use internal representations for afster performance 2026-05-28 09:05:49 +02:00
khwezi 6ae63e2ad1 Merge pull request 'midrandshop' (#42) from midrandshop into master
Reviewed-on: #42
2026-05-27 09:15:44 +02:00
khwezi f5efdde37c Merge pull request 'Refactored connection string name' (#41) from midrandshop into master
Reviewed-on: #41
2026-05-24 13:51:11 +02:00
khwezi 1592d5dc8f Merge pull request 'midrandshop' (#40) from midrandshop into master
Reviewed-on: #40
2026-05-24 13:43:22 +02:00
khwezi 50a8a59d92 Merge pull request 'Split Features to create space for more projects' (#39) from midrandshop into master
Reviewed-on: #39
2026-05-24 13:19:55 +02:00
khwezi b70d9559b0 Merge pull request 'midrandshop' (#38) from midrandshop into master
Reviewed-on: #38
2026-05-23 11:55:02 +02:00
khwezi 81d5e8f07c Merge pull request 'Added MidrandShop feature and spl;it extensions and healthchecks' (#37) from midrandshop into master
Reviewed-on: #37
2026-05-23 11:49:27 +02:00
khwezi 20a53942b5 Merge pull request 'Added product metadata' (#36) from s3service into master
Reviewed-on: #36
2026-05-20 21:15:01 +02:00
khwezi 9edb2aa4aa Merge pull request 'Optimised UploadFileAsync()' (#35) from s3service into master
Reviewed-on: #35
2026-05-20 15:33:47 +02:00
khwezi 6ed023f2cf Merge pull request 'Refactored the S3 services to properly upload the file' (#34) from s3service into master
Reviewed-on: #34
2026-05-20 08:03:43 +02:00
khwezi 2c9f5a846c Merge pull request 'Updated how i use configs' (#33) from s3service into master
Reviewed-on: #33
2026-05-19 14:57:59 +02:00
khwezi 41f7c05be3 Merge pull request 'Refactored service to internalise the CDN' (#32) from s3service into master
Reviewed-on: #32
2026-05-19 11:34:51 +02:00
khwezi 1a03355e84 Merge pull request 'Added S3 support' (#31) from s3service into master
Reviewed-on: #31
2026-05-19 10:24:05 +02:00
khwezi 7743c3178e Merge pull request 'Simplified notification updating' (#30) from emailjobs into master
Reviewed-on: #30
2026-05-17 16:01:24 +02:00
khwezi ab3d8e6e9a Merge pull request 'Refactored GetNotificationsAsync() date handling' (#29) from emailjobs into master
Reviewed-on: #29
2026-05-17 13:14:01 +02:00
khwezi db4c348288 Merge pull request 'Fixed email sending logic' (#28) from emailjobs into master
Reviewed-on: #28
2026-05-16 00:29:01 +02:00
khwezi 6683234642 Merge pull request 'Refactored batch drop logic' (#27) from emailjobs into master
Reviewed-on: #27
2026-05-16 00:05:51 +02:00
khwezi 6ddbb9479a Merge pull request 'Added an empty constructor to ProcessEmailNotificationEvent' (#26) from emailjobs into master
Reviewed-on: #26
2026-05-15 23:53:09 +02:00
khwezi 6c7349a0f8 Merge pull request 'Added additional logging and traces' (#25) from emailjobs into master
Reviewed-on: #25
2026-05-15 23:21:52 +02:00
khwezi e97fd6cd3f Merge pull request 'Added debug logging' (#24) from emailjobs into master
Reviewed-on: #24
2026-05-15 23:09:21 +02:00
khwezi 184c7c252a Merge pull request 'Set misfireThreshold to 2min and eased Cluster node checkin limit' (#23) from emailjobs into master
Reviewed-on: #23
2026-05-15 22:29:08 +02:00
khwezi bfe8c458d6 Merge pull request 'Optimised quartz' (#22) from emailjobs into master
Reviewed-on: #22
2026-05-15 09:52:06 +02:00
khwezi e6e0475db1 Merge pull request 'emailjobs' (#21) from emailjobs into master
Reviewed-on: #21
2026-05-15 08:39:36 +02:00
khwezi 5090c60797 Merge pull request 'Fixed Lead->Customer Relationship' (#20) from emailjobs into master
Reviewed-on: #20
2026-05-15 07:55:56 +02:00
khwezi 9432252e15 Merge pull request 'Added khongisa host entry on pipeline commands' (#19) from emailjobs into master
Reviewed-on: #19
2026-05-14 02:54:00 +02:00
khwezi 47111a1a3a Merge pull request 'emailjobs' (#18) from emailjobs into master
Reviewed-on: #18
2026-05-14 02:49:34 +02:00
khwezi 6eb3d50375 Merge pull request 'Updated job scheduler' (#17) from emailjobs into master
Reviewed-on: #17
2026-05-10 17:33:23 +02:00
khwezi 4deb732804 Merge pull request 'emailjobs' (#16) from emailjobs into master
Reviewed-on: #16
2026-05-10 16:51:24 +02:00
khwezi 20d9387d0b Merge pull request 'Migrated database changes' (#15) from develop into master
Reviewed-on: #15
2026-05-10 11:17:54 +02:00
khwezi 9f6d0ccaa0 Merge pull request 'Populated README' (#14) from develop into master
Reviewed-on: #14
2026-05-10 09:48:35 +02:00
khwezi 1acbc4d213 Merge pull request 'fixed git repo naming' (#13) from develop into master
Reviewed-on: #13
2026-05-10 09:22:37 +02:00
khwezi 8c99668fac Merge pull request 'Added tag and release step after publish' (#12) from develop into master
Reviewed-on: #12
2026-05-10 09:19:33 +02:00
khwezi ad44f46204 Merge pull request 'Refactored Quartz instance id to AUTO, removed constant' (#11) from develop into master
Reviewed-on: #11
2026-05-10 08:21:08 +02:00
khwezi 49d999c1e3 Merge pull request 'Refactored postgres extension' (#10) from develop into master
Reviewed-on: #10
2026-05-09 17:45:00 +02:00
khwezi 9ed4777a18 Merge pull request 'Refactored database references' (#9) from develop into master
Reviewed-on: #9
2026-05-09 17:01:02 +02:00
khwezi 0cf44f68cc Merge pull request 'Added scheduler constants' (#8) from develop into master
Reviewed-on: #8
2026-05-09 15:27:50 +02:00
khwezi 41ed5a4288 Merge pull request 'Fixed quartz host config table prefix' (#7) from develop into master
Reviewed-on: #7
2026-05-09 13:30:33 +02:00
khwezi bbcba5e06c Merge pull request 'Fixed quartz table name prefix' (#6) from develop into master
Reviewed-on: #6
2026-05-09 13:28:29 +02:00
khwezi 502cc326dd Merge pull request 'Updated pipeline to use major version with minor always 0' (#5) from develop into master
Reviewed-on: #5
2026-05-09 12:02:19 +02:00
khwezi 4675d4c5fc Merge pull request 'Updated pipeline to use major versions only' (#4) from develop into master
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
Reviewed-on: #4
2026-05-09 11:56:20 +02:00
khwezi f80bb2fff9 Merge pull request 'Changed target branch from nain to master' (#3) from dronepipeline into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: #3
2026-05-09 11:37:39 +02:00
khwezi 6767906b0d Merge pull request 'Added pipeline separator' (#2) from dronepipeline into master
Reviewed-on: #2
2026-05-09 11:25:34 +02:00
khwezi a344af4498 Merge pull request 'Added .drone.yml pipeline' (#1) from dronepipeline into master
Reviewed-on: #1
2026-05-09 11:12:38 +02:00
46 changed files with 2295 additions and 522 deletions
@@ -0,0 +1,14 @@
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
public class CdnSettings
{
public string? BaseCdn { get; set; }
public string[]? BookCovers { get; set; }
public string[]? Authors { get; set; }
public string[]? AuthorThumbnails { get; set; }
public string[]? BookThumbnails { get; set; }
}
@@ -0,0 +1,275 @@
using LiteCharms.Features.MidrandBooks.Customers;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Orders;
using LiteCharms.Features.MidrandBooks.Orders.Models;
namespace LiteCharms.Features.MidrandBooks.Seed;
public class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
ILogger<CustomerSeederService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!await features.IsEnabledAsync("CustomerSeederService")) return;
logger.LogInformation("Customer Seeding started");
// 1: Add shipping providers (shippingProvider IDs will be created in sequence: 1, 2, 3)
await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 39, "https://www.fastway.co.za/our-services/track-your-parcel"), stoppingToken);
await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.DHL, "DHL Couriers", 60, "https://www.dhl.com/za-en/home/tracking.html"), stoppingToken);
await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.PostNet, "Postnet Overnight Mail", 45, "https://www.postnet.co.za/tracker"), stoppingToken);
// Initialize Bogus Faker engine
var faker = new Faker();
var culture = CultureInfo.InvariantCulture;
// Ensure repeatable datasets across executions
Randomizer.Seed = new Random(84);
// South African Provinces array lookup helper
var southAfricanProvinces = new[]
{
"Gauteng", "Western Cape", "KwaZulu-Natal", "Eastern Cape",
"Free State", "Limpopo", "Mpumalanga", "North West", "Northern Cape"
};
// South African major towns matching geographic boundaries roughly
var southAfricanCities = new[] { "Midrand", "Johannesburg", "Pretoria", "Cape Town", "Durban", "Gqeberha", "Polokwane", "Nelspruit", "Bloemfontein" };
// Tracks sequential Address IDs added globally to the system across all loops
long addressSequenceCounter = 0;
// 2: Create 15 customers with resources sequentially
for (int c = 0; c < 15; c++)
{
if (stoppingToken.IsCancellationRequested) break;
// Determine if this specific iteration represents a Corporate Client or an Individual Consumer
bool isCompanyCustomer = faker.Random.Bool(0.4f); // 40% chance of seeding a corporate entity
string customerFirstName = faker.Name.FirstName();
string customerLastName = faker.Name.LastName();
string companyName = isCompanyCustomer ? faker.Company.CompanyName() : "";
string companySuffix = isCompanyCustomer ? faker.Company.CompanySuffix() : "";
string fullCompanyName = isCompanyCustomer ? $"{companyName} {companySuffix}" : "";
string customerEmail = isCompanyCustomer
? faker.Internet.Email(firstName: companyName, provider: "co.za").ToLower(culture)
: faker.Internet.Email(customerFirstName, customerLastName).ToLower(culture);
string customerPhone = faker.Phone.PhoneNumber("087#######"); // Corporate VOIP / Personal South African cell line format
string customerWebsite = isCompanyCustomer ? faker.Internet.Url().Replace("www.", $"www.{companyName.ToLower(culture)}.") : "";
string customerVat = isCompanyCustomer ? faker.Phone.PhoneNumber("4#########") : ""; // SA VAT registration starts with a 4
// Randomly select distinct Social Media channels
var chosenSocialType = faker.PickRandom<SocialMediaTypes>();
string socialMediaUrl = chosenSocialType switch
{
SocialMediaTypes.LinkedIn => isCompanyCustomer ? $"https://linkedin.com/company/{companyName.ToLower(culture)}" : $"https://linkedin.com/in/{customerFirstName.ToLower(culture)}-{customerLastName.ToLower(culture)}",
SocialMediaTypes.GitHub => $"https://github.com/{(isCompanyCustomer ? "orgs/" + companyName.ToLower(culture) : customerFirstName.ToLower(culture))}",
_ => $"https://x.com/{(isCompanyCustomer ? companyName.ToLower(culture) : customerFirstName.ToLower(culture))}"
};
// 3: Create customer
var createCustomerResult = await customerService.CreateCustomerAsync(new CreateCustomer
{
Company = fullCompanyName,
Email = customerEmail,
Phone = customerPhone,
Website = customerWebsite,
SocialMedia =
[
new Models.SocialMedia
{
Name = chosenSocialType.ToString(),
Type = chosenSocialType,
ImageUrl = $"https://cdn.example.com/icons/{chosenSocialType.ToString().ToLower(culture)}.png",
Url = socialMediaUrl
}
],
VatNumber = customerVat
}, stoppingToken);
if (createCustomerResult.IsFailed)
{
logger.LogError("Failed to create customer record at index {Index}: {Error}", c, createCustomerResult.Errors[0].Message);
break;
}
var assignedCustomerId = createCustomerResult.Value;
// 4: Create customer contact (only if customer is a company entity)
if (isCompanyCustomer)
{
var contactFirstName = faker.Name.FirstName();
var contactLastName = faker.Name.LastName();
var createContactResult = await customerService.CreateCustomerContactAsync(assignedCustomerId, new CreateCustomerContact
{
Name = contactFirstName,
LastName = contactLastName,
Phone = faker.Phone.PhoneNumber("082#######"), // Typical South African mobile prefix format
Email = faker.Internet.Email(contactFirstName, contactLastName, provider: "company.co.za").ToLower(culture),
Type = ContactTypes.Business
}, stoppingToken);
if (createContactResult.IsFailed)
{
logger.LogError("Failed to create company customer contact relation: {Error}", createContactResult.Errors[0].Message);
break;
}
}
// Shared Randomizations for Regional Postal/Building details
var primaryState = faker.PickRandom(southAfricanProvinces);
var primaryCity = faker.PickRandom(southAfricanCities);
var shippingPostalCode = faker.Random.Replace("####");
var billingState = faker.PickRandom(southAfricanProvinces);
var billingCity = faker.PickRandom(southAfricanCities);
var billingPostalCode = faker.Random.Replace("####");
// 5: Create customer address - SHIPPING
var createShippingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress
{
Name = isCompanyCustomer ? "Head Office Distribution" : "My Home Residence",
BuildingType = faker.PickRandom<AddressBuildingTypes>(),
Type = AddressType.Shipping,
Street = $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street",
City = primaryCity,
State = primaryState,
Country = "South Africa",
IsPrimary = true,
Enabled = true,
PostalCode = shippingPostalCode
}, stoppingToken);
long currentCustomerShippingAddressId = 0;
if (createShippingAddressResult.IsSuccess)
{
addressSequenceCounter++;
currentCustomerShippingAddressId = addressSequenceCounter;
}
else
{
logger.LogWarning("Failed to attach Shipping address profile: {Error}", createShippingAddressResult.Errors[0].Message);
}
// 6: Create customer address - BILLING
var createBillingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress
{
Name = isCompanyCustomer ? "Accounts Payable Department" : "Billing Address",
BuildingType = faker.PickRandom<AddressBuildingTypes>(),
Type = AddressType.Billing,
Street = isCompanyCustomer ? $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Boulevard" : $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street",
City = billingCity,
State = billingState,
Country = "South Africa",
IsPrimary = false,
Enabled = true,
PostalCode = billingPostalCode
}, stoppingToken);
long currentCustomerBillingAddressId = 0;
if (createBillingAddressResult.IsSuccess)
{
addressSequenceCounter++;
currentCustomerBillingAddressId = addressSequenceCounter;
}
else
{
logger.LogError("Failed to attach Billing address profile: {Error}", createBillingAddressResult.Errors[0].Message);
break;
}
// 7: Challenge Extrapolation — Create a random number of orders (0 to 4 orders) per customer
int ordersToGenerate = faker.Random.Number(0, 4);
for (int o = 0; o < ordersToGenerate; o++)
{
var deliveryInstructions = faker.PickRandom(
"Leave at reception desk",
"Please call before delivery",
"At the intercom, dial 1 then option 2",
"Leave with security guard at front gate",
"Deliver to back delivery bay"
);
// Use the calculated sequential Billing Address Id for order creation
var orderResult = await orderService.CreateOrderAsync(
assignedCustomerId,
new CreateOrder(currentCustomerBillingAddressId, deliveryInstructions),
stoppingToken
);
if (orderResult.IsFailed)
{
logger.LogWarning("Failed to create purchase order shell context: {Error}", orderResult.Errors[0].Message);
continue;
}
long seededOrderId = orderResult.Value;
// Build a varying array of items using valid product bounds (IDs: 0 to 21)
int lineItemsCount = faker.Random.Number(1, 5);
var itemsList = new List<CreateOrderItem>();
for (int i = 0; i < lineItemsCount; i++)
{
long randomProductId = faker.Random.Number(0, 21);
long randomProductPriceId = faker.Random.Number(0, 21);
int itemQuantity = faker.Random.Number(1, 3);
itemsList.Add(new CreateOrderItem(randomProductId, randomProductPriceId, itemQuantity));
}
// Push bulk items payload into order via matching test framework signatures
var addItemsResult = await orderService.AddItemsToOrderAsync(seededOrderId, [.. itemsList], stoppingToken);
if (addItemsResult.IsFailed)
{
logger.LogWarning("Failed to link item collections to Order Id {Id}", seededOrderId);
continue;
}
// Randomly select an order status matrix pathing
var targetedOrderStatus = faker.PickRandom<OrderStatus>();
await orderService.UpdateOrderStatusAsync(seededOrderId, targetedOrderStatus, stoppingToken);
// Check lifecycle workflow criteria: Attach dynamic shipping if status warrants it
if (targetedOrderStatus != OrderStatus.Pending &&
targetedOrderStatus != OrderStatus.Cancelled &&
targetedOrderStatus != OrderStatus.Failed &&
currentCustomerShippingAddressId > 0)
{
// Select from seeded Shipping Providers in step 1 (IDs: 1, 2, or 3)
long randomShippingProviderId = faker.Random.Number(1, 3);
var addShippingResult = await orderService.AddShippingToOrderAsync(
seededOrderId,
new CreateShipping(currentCustomerShippingAddressId, randomShippingProviderId),
stoppingToken
);
if (addShippingResult.IsSuccess)
{
long assignedShippingId = addShippingResult.Value;
// Transition logistics flags matching delivery metrics
var shippingStatus = faker.PickRandom<ShippingStatuses>();
await orderService.UpdateShippingStatusAsync(seededOrderId, shippingStatus, stoppingToken);
if (shippingStatus == ShippingStatuses.Shipped || shippingStatus == ShippingStatuses.Delivered)
{
string rawTrackingCode = $"ZA{faker.Random.Replace("#########")}NV";
await orderService.UpdateShippingTrackingNumberAsync(seededOrderId, assignedShippingId, rawTrackingCode);
}
}
}
}
logger.LogInformation("Successfully seeded customer profile #{Index}: {Name} alongside {Count} orders.", c, isCompanyCustomer ? fullCompanyName : $"{customerFirstName} {customerLastName}", ordersToGenerate);
}
logger.LogInformation("Customer Seeding completed successfully.");
}
}
@@ -0,0 +1,160 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>5c3bc894-8654-4691-99e8-f90d3414843f</UserSecretsId>
</PropertyGroup>
<!-- Quartz Scheduler-->
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.5" />
<PackageReference Include="Meziantou.Analyzer" Version="3.0.98">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="4.5.0" />
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
<PackageReference Include="Quartz" Version="3.18.1" />
<PackageReference Include="Quartz.Plugins" Version="3.18.1" />
<PackageReference Include="Quartz.Plugins.TimeZoneConverter" Version="3.18.1" />
<PackageReference Include="Quartz.Serialization.SystemTextJson" Version="3.18.1" />
<PackageReference Include="Quartz.AspNetCore" Version="3.18.1" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
<!-- Global Usings -->
<Using Include="Quartz" />
</ItemGroup>
<!-- Configuration -->
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.8" />
<!-- Global Usings -->
<Using Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<!-- Health Checks -->
<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Core" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.Data" Version="9.0.0" />
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.8" />
<!-- Global Usings -->
<Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
<Using Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
</ItemGroup>
<!-- Open Telemetry -->
<ItemGroup>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
<!-- Global Usings -->
<Using Include="OpenTelemetry.Resources" />
<Using Include="OpenTelemetry.Exporter" />
<Using Include="OpenTelemetry.Logs" />
<Using Include="OpenTelemetry.Metrics" />
<Using Include="OpenTelemetry.Trace" />
</ItemGroup>
<!-- Database -->
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
<!-- Global Usings -->
<Using Include="Npgsql" />
<Using Include="Microsoft.EntityFrameworkCore" />
<Using Include="Microsoft.EntityFrameworkCore.Design" />
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
</ItemGroup>
<!-- Email -->
<ItemGroup>
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="MimeKit" Version="4.17.0" />
<!-- Global Usings-->
<Using Include="MimeKit" />
<Using Include="MailKit.Net.Smtp" />
</ItemGroup>
<!-- CQRS -->
<ItemGroup>
<PackageReference Include="FluentResults" Version="4.0.0" />
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
<!-- Global Usings -->
<Using Include="FluentResults" />
<Using Include="Mediator" />
</ItemGroup>
<!-- Amazon S3 SDK -->
<ItemGroup>
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.4.1" />
<PackageReference Include="AWSSDK.S3" Version="4.0.23.4" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
<!-- global Usings -->
<Using Include="Amazon.S3" />
<Using Include="Amazon.S3.Model" />
<Using Include="Amazon.Runtime" />
</ItemGroup>
<!-- Shared Usings -->
<ItemGroup>
<Using Include="Bogus" />
<Using Include="Microsoft.FeatureManagement" />
<Using Include="System.Globalization" />
<Using Include="System.Reflection" />
<Using Include="Microsoft.AspNetCore.Builder" />
<Using Include="Microsoft.Extensions.Hosting" />
<Using Include="System.Text" />
<Using Include="System.Text.Json" />
<Using Include="System.Threading.Channels" />
<Using Include="System.Collections.ObjectModel" />
<Using Include="System.Diagnostics" />
<Using Include="System.Diagnostics.Metrics" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="System.Security.Cryptography" />
<Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Features.MidrandBooks\LiteCharms.Features.MidrandBooks.csproj" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,246 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks;
using LiteCharms.Features.MidrandBooks.Authors;
using LiteCharms.Features.MidrandBooks.Products;
using LiteCharms.Features.MidrandBooks.Seed.Configuration;
namespace LiteCharms.Features.MidrandBooks.Seed;
public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
{
private readonly CdnSettings cdnSettings = options.Value;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (await features.IsEnabledAsync("ProductsSeederService") is not true) return;
logger.LogInformation("Product Seeding started");
if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0)
{
logger.LogWarning("No book covers found in CDN settings. Seeding aborted.");
return;
}
// Initialize Bogus Faker engine
var faker = new Faker();
var culture = CultureInfo.InvariantCulture;
// Ensure repeatable data sets if run multiple times by anchoring the seed
Randomizer.Seed = new Random(42);
foreach (var bookCover in cdnSettings.BookCovers)
{
if (stoppingToken.IsCancellationRequested) break;
// Generate beautifully mixed eclectic topics on the fly
var bookTopic = faker.PickRandom(
// --- Tech & IT ---
"C# 12 & Modern .NET Architecture",
"PostgreSQL Database Optimization",
"Docker & Kubernetes in Production",
"Domain-Driven Design Paradigms",
"Artificial Intelligence with Python",
// --- Sci-Fi & Fantasy ---
"The Chronicles of the Quantum Nebula",
"Legends of the Lost Cybernetic Kingdom",
"Parallel Dimensions and Rogue Time Streams",
"The Last Android in Neo-Johannesburg",
// --- Thrillers, Mystery & Crime ---
"The Midnight Code Cryptograph",
"Shadows in the Highveld",
"The Silent Witness of Midrand",
"Deception on the 14th Floor",
// --- Business, Finance & Wealth ---
"Mastering the South African Tech Market",
"The Modern Entrepreneur's Blueprint",
"Generational Wealth and Venture Capital",
"Negotiation Tactics for High-Stakes Deals",
// --- Self-Help & Personal Growth ---
"The Art of Relentless Focus",
"Building High-Performance Habits",
"The Mindfulness Guide for Software Engineers",
"Unlocking Creative Flow Under Pressure"
);
// Dynamic raw title generation formulas executed via random function picker
var titlePatterns = new Func<string>[]
{
() => $"{faker.Company.CatchPhrase()} with {bookTopic}",
() => $"The {faker.Commerce.ProductAdjective()} Guide to {bookTopic}",
() => $"Mastering {bookTopic}: A {faker.Company.Bs()} Blueprint",
() => $"{bookTopic} for the Modern {faker.Name.JobTitle()}",
() => $"Advanced {bookTopic}: Demystifying the {faker.Company.CatchPhrase()}",
() => $"{faker.Random.Replace("###")} Blueprints for {bookTopic}"
};
// Pick a format template and resolve it down to raw string text
var rawTitle = faker.PickRandom(titlePatterns)();
var bookTitle = rawTitle.Length > 255 ? rawTitle[..252] + "..." : rawTitle;
var rawSummary = $"A comprehensive guide to mastering {bookTopic}. Learn modern implementation techniques through real-world software engineering paradigms.";
var bookSummary = rawSummary.Length > 512 ? rawSummary[..509] + "..." : rawSummary;
// Generating a single concise paragraph ensures a rich text description falling safely well under 1024
var rawDescription = faker.Lorem.Paragraph(3);
var bookDescription = rawDescription.Length > 1024 ? rawDescription[..1021] + "..." : rawDescription;
var authorFirstName = faker.Name.FirstName();
var authorLastName = faker.Name.LastName();
var publisherCompany = faker.Company.CompanyName();
// Safe bounded random picking for book thumbnails
string? pickedBookThumbnail = null;
string? pickedBookThumbnail1 = null;
string? pickedBookThumbnail2 = null;
string? pickedBookThumbnail3 = null;
string? pickedBookThumbnail4 = null;
if (cdnSettings.BookThumbnails is not null && cdnSettings.BookThumbnails.Length > 0)
{
pickedBookThumbnail = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
pickedBookThumbnail1 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
pickedBookThumbnail2 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
pickedBookThumbnail3 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
pickedBookThumbnail4 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
}
// Step 1: Add Product
var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct
{
Name = bookTitle,
Summary = bookSummary,
Description = bookDescription,
ImageUrl = $"{cdnSettings.BaseCdn}{bookCover}",
Type = ProductTypes.Book,
Metadata = new Models.ProductMetadata
{
CopyrightInfo = $"© {DateTime.UtcNow.Year} {publisherCompany}. All rights reserved.",
ManufactureDate = faker.Date.Past(3).ToString("yyyy-MM-dd", culture),
Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}",
SerialNumber = faker.Phone.PhoneNumber("978-##########")
},
Categories = ["Coding", "Computers", "IT"],
ThumbnailUrls = pickedBookThumbnail is not null ? [pickedBookThumbnail, pickedBookThumbnail1!, pickedBookThumbnail2!, pickedBookThumbnail3!, pickedBookThumbnail4!] : null
}, stoppingToken);
if (productCreateResult.IsFailed)
{
logger.LogError("Failed to create product: {Error}", productCreateResult.Errors[0].Message);
break;
}
// Step 2: Enable product so it can show on the shop
var enableProductResult = await productService.UpdateProductStatusAsync(productId: productCreateResult.Value, isEnabled: true, stoppingToken);
if (enableProductResult.IsFailed)
{
logger.LogError("Failed to enable created product: {Error}", enableProductResult.Errors[0].Message);
break;
}
// Step 3: Create Product Price
var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice
{
Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2),
Discount = 0.0m
}, stoppingToken);
if (productPriceCreateResult.IsFailed)
{
logger.LogError("Failed to create product price: {Error}", productPriceCreateResult.Errors[0].Message);
break;
}
// Safe bounded picking for Authors (Real Avatars)
string authorAvatarUrl = faker.Internet.Avatar(); // Fallback
if (cdnSettings.Authors is not null && cdnSettings.Authors.Length > 0)
{
authorAvatarUrl = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.Authors)}";
}
// Safe bounded picking for Author Thumbnails (Cartoon Avatars)
string? authorThumbnailUrl = null;
if (cdnSettings.AuthorThumbnails is not null && cdnSettings.AuthorThumbnails.Length > 0)
{
var selectedThumb = faker.PickRandom(cdnSettings.AuthorThumbnails);
authorThumbnailUrl = $"{cdnSettings.BaseCdn}{selectedThumb}.jpg";
}
// Synthesize a highly dynamic, organic opening bio statement
var professionalBackgrounds = new[]
{
$"{authorFirstName} {authorLastName} is an award-winning {faker.Name.JobDescriptor()} {faker.Name.JobTitle()} with over {faker.Random.Number(5, 25)} years of core engineering domain expertise.",
$"As a veteran systems consultant and practicing {faker.Name.JobTitle()}, {authorFirstName} has spent decades leading digital infrastructure transformations and managing complex topologies.",
$"Operating from modern innovation hubs, {authorFirstName} {authorLastName} specializes in global product strategies and serves as an authority in {faker.Name.JobDescriptor()} computing.",
$"With a rich professional background as a principal {faker.Name.JobTitle()} at {publisherCompany}, {authorFirstName} has spent a lifetime refining the system workflows highlighted here."
};
// Pick a randomized context hook and append a 2-paragraph contextual narrative block
var biographyPrefix = faker.PickRandom(professionalBackgrounds);
var authorBiography = $"{biographyPrefix} {faker.Lorem.Paragraph(2)}";
// Step 4: Create Author
var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor
{
Name = authorFirstName,
LastName = authorLastName,
Company = publisherCompany,
VatNumber = faker.Random.Bool() ? faker.Phone.PhoneNumber("4#########") : "",
PublisherType = faker.PickRandom<PublisherTypes>(),
Email = faker.Internet.Email(authorFirstName, authorLastName),
Website = faker.Internet.Url(),
ImageUrl = authorAvatarUrl,
SocialMedia =
[
new Models.SocialMedia
{
Name = "LinkedIn",
ImageUrl = "https://cdn.example.com/icons/linkedin.png",
Type = SocialMediaTypes.LinkedIn,
Url = $"https://linkedin.com/in/{authorFirstName.ToLower(culture)}-{authorLastName.ToLower(culture)}"
},
new Models.SocialMedia
{
Name = "GitHub",
ImageUrl = "https://cdn.example.com/icons/github.png",
Type = SocialMediaTypes.GitHub,
Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}"
}
],
Biography = authorBiography,
ThumbnailImageUrl = authorThumbnailUrl
}, stoppingToken);
if (authorCreateResult.IsFailed)
{
logger.LogError("Failed to create author: {Error}", authorCreateResult.Errors[0].Message);
break;
}
// Step 5: Create Author-Book link (product linkage)
var authorBookCreateResult = await booksService.CreateBookAsync(authorId: authorCreateResult.Value, productId: productCreateResult.Value, stoppingToken);
if (authorBookCreateResult.IsFailed)
{
logger.LogError("Failed to create author-book linkage: {Error}", authorBookCreateResult.Errors[0].Message);
break;
}
var enableAuthorBookResult = await booksService.UpdateBookStatusAsync(bookId: authorBookCreateResult.Value, isEnabled: true, stoppingToken);
if (enableAuthorBookResult.IsFailed)
{
logger.LogError("Failed to enable author-book link: {Error}", enableAuthorBookResult.Errors[0].Message);
break;
}
logger.LogInformation("Successfully seeded book product: {Title}", bookTitle);
}
logger.LogInformation("Product Seeding completed successfully.");
}
}
@@ -0,0 +1,26 @@
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Seed;
using LiteCharms.Features.MidrandBooks.Seed.Configuration;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.AddCommandLine(args)
.AddUserSecrets(typeof(Program).Assembly)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
builder.Services.AddScopedFeatureManagement();
builder.Services
.AddLogging()
.AddShopServices()
.AddHostedService<ProductsSeederService>()
.AddHostedService<CustomerSeederService>()
.AddMidrandShopDatabase(builder.Configuration);
builder.Services.Configure<CdnSettings>(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options));
using var host = builder.Build();
await host.RunAsync();
@@ -0,0 +1,265 @@
{
"FeatureManagement": {
"CustomerSeederService": false,
"ProductsSeederService": false
},
"CdnSettings": {
"BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/",
"BookCovers": [
"144e0314-0bd8-4e2c-8814-2b34608c0600_1764780116467.webp",
"2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp",
"3cd172b9-416e-4b3a-b613-7ff0a3aecc4c_1764780129459.webp",
"762947b6-63ac-436e-98fb-91dd94de1d82_1764780126141.webp",
"7b42f23f-666c-4dd9-82e1-9b928e1b4db7_1764780186376.webp",
"8e281eea-8910-473d-b650-43f539995d9e_1764780152316.webp",
"91d18e2c-6ee6-44b4-84a5-8692db5dbfe4_1764780114484.webp",
"94fdf403-4d10-4cb4-a537-fc914a1f5ba8_1764780198423.webp",
"9b881786-75e1-47f7-9bc9-27188d46d1ec_1764780122458.webp",
"c417236d-a628-45eb-82c2-ec1cb0326e4f_1765006317965.webp",
"clpqjsgi71jpr1i6392e449l2.webp",
"clps1mk9f0m1z1i32grvq17tg.webp",
"clq550xxy2dab1ywb6mdv4acv.webp",
"clq5535o52dj51yw557ml49c1.webp",
"clq55455w2dcm1yvd8d8258d8.webp",
"clq558fkh2djy1ywbghwcawnh.webp",
"clq55cvqh2dqi1yyratl123wq.webp",
"clrhnk94e115k1y00bg8h9ixb.webp",
"d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp",
"e6ba52f208914285bcdf1966cfb08f6f.jpg",
"fa9cbbe6-f947-4f83-8e98-61d2661f43e0_1764841636705.webp"
],
"Authors": [
"authors/uifaces-human-avatar.jpg",
"authors/uifaces-human-avatar-1.jpg",
"authors/uifaces-human-avatar-2.jpg",
"authors/uifaces-human-avatar-3.jpg",
"authors/uifaces-human-avatar-4.jpg",
"authors/uifaces-human-avatar-5.jpg",
"authors/uifaces-human-avatar-6.jpg",
"authors/uifaces-human-avatar-7.jpg",
"authors/uifaces-human-avatar-8.jpg",
"authors/uifaces-human-avatar-9.jpg",
"authors/uifaces-human-avatar-10.jpg",
"authors/uifaces-human-avatar-11.jpg",
"authors/uifaces-human-avatar-12.jpg",
"authors/uifaces-human-avatar-13.jpg",
"authors/uifaces-human-avatar-14.jpg",
"authors/uifaces-human-avatar-15.jpg",
"authors/uifaces-human-avatar-16.jpg"
],
"AuthorThumbnails": [
"authors/thumbnails/uifaces-cartoon-avatar-1",
"authors/thumbnails/uifaces-cartoon-avatar-2",
"authors/thumbnails/uifaces-cartoon-avatar-3",
"authors/thumbnails/uifaces-cartoon-avatar-4",
"authors/thumbnails/uifaces-cartoon-avatar-5",
"authors/thumbnails/uifaces-cartoon-avatar-6",
"authors/thumbnails/uifaces-cartoon-avatar-7",
"authors/thumbnails/uifaces-cartoon-avatar-8",
"authors/thumbnails/uifaces-cartoon-avatar-9",
"authors/thumbnails/uifaces-cartoon-avatar-10"
],
"BookThumbnails": [
"thumbnails/book_thumbnail_001.jpg",
"thumbnails/book_thumbnail_002.jpg",
"thumbnails/book_thumbnail_003.jpg",
"thumbnails/book_thumbnail_004.jpg",
"thumbnails/book_thumbnail_005.jpg",
"thumbnails/book_thumbnail_006.jpg",
"thumbnails/book_thumbnail_007.jpg",
"thumbnails/book_thumbnail_008.jpg",
"thumbnails/book_thumbnail_009.jpg",
"thumbnails/book_thumbnail_010.jpg",
"thumbnails/book_thumbnail_011.jpg",
"thumbnails/book_thumbnail_012.jpg",
"thumbnails/book_thumbnail_013.jpg",
"thumbnails/book_thumbnail_014.jpg",
"thumbnails/book_thumbnail_015.jpg",
"thumbnails/book_thumbnail_016.jpg",
"thumbnails/book_thumbnail_017.jpg",
"thumbnails/book_thumbnail_018.jpg",
"thumbnails/book_thumbnail_019.jpg",
"thumbnails/book_thumbnail_020.jpg",
"thumbnails/book_thumbnail_021.jpg",
"thumbnails/book_thumbnail_022.jpg",
"thumbnails/book_thumbnail_023.jpg",
"thumbnails/book_thumbnail_024.jpg",
"thumbnails/book_thumbnail_025.jpg",
"thumbnails/book_thumbnail_026.jpg",
"thumbnails/book_thumbnail_027.jpg",
"thumbnails/book_thumbnail_028.jpg",
"thumbnails/book_thumbnail_029.jpg",
"thumbnails/book_thumbnail_030.jpg",
"thumbnails/book_thumbnail_031.jpg",
"thumbnails/book_thumbnail_032.jpg",
"thumbnails/book_thumbnail_033.jpg",
"thumbnails/book_thumbnail_034.jpg",
"thumbnails/book_thumbnail_035.jpg",
"thumbnails/book_thumbnail_036.jpg",
"thumbnails/book_thumbnail_037.jpg",
"thumbnails/book_thumbnail_038.jpg",
"thumbnails/book_thumbnail_039.jpg",
"thumbnails/book_thumbnail_040.jpg",
"thumbnails/book_thumbnail_041.jpg",
"thumbnails/book_thumbnail_042.jpg",
"thumbnails/book_thumbnail_043.jpg",
"thumbnails/book_thumbnail_044.jpg",
"thumbnails/book_thumbnail_045.jpg",
"thumbnails/book_thumbnail_046.jpg",
"thumbnails/book_thumbnail_047.jpg",
"thumbnails/book_thumbnail_048.jpg",
"thumbnails/book_thumbnail_049.jpg",
"thumbnails/book_thumbnail_050.jpg",
"thumbnails/book_thumbnail_051.jpg",
"thumbnails/book_thumbnail_052.jpg",
"thumbnails/book_thumbnail_053.jpg",
"thumbnails/book_thumbnail_054.jpg",
"thumbnails/book_thumbnail_055.jpg",
"thumbnails/book_thumbnail_056.jpg",
"thumbnails/book_thumbnail_057.jpg",
"thumbnails/book_thumbnail_058.jpg",
"thumbnails/book_thumbnail_059.jpg",
"thumbnails/book_thumbnail_060.jpg",
"thumbnails/book_thumbnail_061.jpg",
"thumbnails/book_thumbnail_062.jpg",
"thumbnails/book_thumbnail_063.jpg",
"thumbnails/book_thumbnail_064.jpg",
"thumbnails/book_thumbnail_065.jpg",
"thumbnails/book_thumbnail_066.jpg",
"thumbnails/book_thumbnail_067.jpg",
"thumbnails/book_thumbnail_068.jpg",
"thumbnails/book_thumbnail_069.jpg",
"thumbnails/book_thumbnail_070.jpg",
"thumbnails/book_thumbnail_071.jpg",
"thumbnails/book_thumbnail_072.jpg",
"thumbnails/book_thumbnail_073.jpg",
"thumbnails/book_thumbnail_074.jpg",
"thumbnails/book_thumbnail_075.jpg",
"thumbnails/book_thumbnail_076.jpg",
"thumbnails/book_thumbnail_077.jpg",
"thumbnails/book_thumbnail_078.jpg",
"thumbnails/book_thumbnail_079.jpg",
"thumbnails/book_thumbnail_080.jpg",
"thumbnails/book_thumbnail_081.jpg",
"thumbnails/book_thumbnail_082.jpg",
"thumbnails/book_thumbnail_083.jpg",
"thumbnails/book_thumbnail_084.jpg",
"thumbnails/book_thumbnail_085.jpg",
"thumbnails/book_thumbnail_086.jpg",
"thumbnails/book_thumbnail_087.jpg",
"thumbnails/book_thumbnail_088.jpg",
"thumbnails/book_thumbnail_089.jpg",
"thumbnails/book_thumbnail_090.jpg",
"thumbnails/book_thumbnail_091.jpg",
"thumbnails/book_thumbnail_092.jpg",
"thumbnails/book_thumbnail_093.jpg",
"thumbnails/book_thumbnail_094.jpg",
"thumbnails/book_thumbnail_095.jpg",
"thumbnails/book_thumbnail_096.jpg",
"thumbnails/book_thumbnail_097.jpg",
"thumbnails/book_thumbnail_098.jpg",
"thumbnails/book_thumbnail_099.jpg",
"thumbnails/book_thumbnail_100.jpg",
"thumbnails/book_thumbnail_101.jpg",
"thumbnails/book_thumbnail_102.jpg",
"thumbnails/book_thumbnail_103.jpg",
"thumbnails/book_thumbnail_104.jpg",
"thumbnails/book_thumbnail_105.jpg",
"thumbnails/book_thumbnail_106.jpg",
"thumbnails/book_thumbnail_107.jpg",
"thumbnails/book_thumbnail_108.jpg",
"thumbnails/book_thumbnail_109.jpg",
"thumbnails/book_thumbnail_110.jpg",
"thumbnails/book_thumbnail_111.jpg",
"thumbnails/book_thumbnail_112.jpg",
"thumbnails/book_thumbnail_113.jpg",
"thumbnails/book_thumbnail_114.jpg",
"thumbnails/book_thumbnail_115.jpg",
"thumbnails/book_thumbnail_116.jpg",
"thumbnails/book_thumbnail_117.jpg",
"thumbnails/book_thumbnail_118.jpg",
"thumbnails/book_thumbnail_119.jpg",
"thumbnails/book_thumbnail_120.jpg",
"thumbnails/book_thumbnail_121.jpg",
"thumbnails/book_thumbnail_122.jpg",
"thumbnails/book_thumbnail_123.jpg",
"thumbnails/book_thumbnail_124.jpg",
"thumbnails/book_thumbnail_125.jpg",
"thumbnails/book_thumbnail_126.jpg",
"thumbnails/book_thumbnail_127.jpg",
"thumbnails/book_thumbnail_128.jpg",
"thumbnails/book_thumbnail_129.jpg",
"thumbnails/book_thumbnail_130.jpg",
"thumbnails/book_thumbnail_131.jpg",
"thumbnails/book_thumbnail_132.jpg",
"thumbnails/book_thumbnail_133.jpg",
"thumbnails/book_thumbnail_134.jpg",
"thumbnails/book_thumbnail_135.jpg",
"thumbnails/book_thumbnail_136.jpg",
"thumbnails/book_thumbnail_137.jpg",
"thumbnails/book_thumbnail_138.jpg",
"thumbnails/book_thumbnail_139.jpg",
"thumbnails/book_thumbnail_140.jpg",
"thumbnails/book_thumbnail_141.jpg",
"thumbnails/book_thumbnail_142.jpg",
"thumbnails/book_thumbnail_143.jpg",
"thumbnails/book_thumbnail_144.jpg",
"thumbnails/book_thumbnail_145.jpg",
"thumbnails/book_thumbnail_146.jpg",
"thumbnails/book_thumbnail_147.jpg",
"thumbnails/book_thumbnail_148.jpg",
"thumbnails/book_thumbnail_149.jpg",
"thumbnails/book_thumbnail_150.jpg",
"thumbnails/book_thumbnail_151.jpg",
"thumbnails/book_thumbnail_152.jpg",
"thumbnails/book_thumbnail_153.jpg",
"thumbnails/book_thumbnail_154.jpg",
"thumbnails/book_thumbnail_155.jpg",
"thumbnails/book_thumbnail_156.jpg",
"thumbnails/book_thumbnail_157.jpg",
"thumbnails/book_thumbnail_158.jpg",
"thumbnails/book_thumbnail_159.jpg",
"thumbnails/book_thumbnail_160.jpg",
"thumbnails/book_thumbnail_161.jpg",
"thumbnails/book_thumbnail_162.jpg",
"thumbnails/book_thumbnail_163.jpg",
"thumbnails/book_thumbnail_164.jpg",
"thumbnails/book_thumbnail_165.jpg",
"thumbnails/book_thumbnail_166.jpg",
"thumbnails/book_thumbnail_167.jpg",
"thumbnails/book_thumbnail_168.jpg",
"thumbnails/book_thumbnail_169.jpg",
"thumbnails/book_thumbnail_170.jpg",
"thumbnails/book_thumbnail_171.jpg",
"thumbnails/book_thumbnail_172.jpg",
"thumbnails/book_thumbnail_173.jpg",
"thumbnails/book_thumbnail_174.jpg",
"thumbnails/book_thumbnail_175.jpg",
"thumbnails/book_thumbnail_176.jpg",
"thumbnails/book_thumbnail_177.jpg",
"thumbnails/book_thumbnail_178.jpg",
"thumbnails/book_thumbnail_179.jpg",
"thumbnails/book_thumbnail_180.jpg",
"thumbnails/book_thumbnail_181.jpg",
"thumbnails/book_thumbnail_182.jpg",
"thumbnails/book_thumbnail_183.jpg",
"thumbnails/book_thumbnail_184.jpg",
"thumbnails/book_thumbnail_185.jpg",
"thumbnails/book_thumbnail_186.jpg",
"thumbnails/book_thumbnail_187.jpg",
"thumbnails/book_thumbnail_188.jpg",
"thumbnails/book_thumbnail_189.jpg",
"thumbnails/book_thumbnail_190.jpg",
"thumbnails/book_thumbnail_191.jpg",
"thumbnails/book_thumbnail_192.jpg",
"thumbnails/book_thumbnail_193.jpg",
"thumbnails/book_thumbnail_194.jpg",
"thumbnails/book_thumbnail_195.jpg",
"thumbnails/book_thumbnail_196.jpg",
"thumbnails/book_thumbnail_197.jpg",
"thumbnails/book_thumbnail_198.jpg",
"thumbnails/book_thumbnail_199.jpg",
"thumbnails/book_thumbnail_200.jpg"
]
}
}
@@ -0,0 +1,81 @@
using LiteCharms.Features.MidrandBooks.Authors;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
[IntegrationFact]
public async Task CreateAuthorAsync_ShouldReturn_ResultWithAuthorId()
{
var request = new CreateAuthor
{
Name = "John",
LastName = "Doe",
Company = "Solo Publishers",
Email = "solo@publishers.co.za",
PublisherType = PublisherTypes.Independent,
ImageUrl = ""
};
var result = await authorService.CreateAuthorAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateAuthorAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateAuthor
{
Name = "Jane",
LastName = "Doe",
Company = "Solo Publishers",
Email = "solo@publishers.co.za",
PublisherType = PublisherTypes.Independent,
ImageUrl = ""
};
var result = await authorService.UpdateAuthorAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetAuthors_ShouldReturn_ResultWithAuthorList()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await authorService.GetAuthorsAsync(range, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetAuthorAsync_ShouldReturn_ResultWithAuthor()
{
var result = await authorService.GetAuthorAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task UpdateAuthorStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await authorService.UpdateAuthorStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -0,0 +1,52 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class BooksServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly BooksService bookService = fixture.Services.GetRequiredService<BooksService>();
[IntegrationFact]
public async Task CreateBookAsync_ShouldReturn_ResultWithBookId()
{
var result = await bookService.CreateBookAsync(1, 2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetBookAsync_ShouldReturn_ResultWithBook()
{
var result = await bookService.GetBookAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetBooksByAuthorAsync_ShouldReturn_ResultWithAuthorBooks()
{
var result = await bookService.GetBooksByAuthorAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetPublishedBooksAsync_ShouldReturn_ResultWithBublishedBooks()
{
var result = await bookService.GetPublishedBooksAsync(0, 1000, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task UpdateBookStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await bookService.UpdateBookStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -1,7 +1,8 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions;
namespace LiteCharms.Features.MidrandBooks.Tests;
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
public class Fixture : IDisposable
{
@@ -11,6 +12,10 @@ public class Fixture : IDisposable
public IMediator Mediator { get; set; }
private readonly CancellationTokenSource cancellationTokenSource = new();
public CancellationToken CancellationToken => cancellationTokenSource.Token;
public Fixture()
{
Configuration = new ConfigurationBuilder()
@@ -23,12 +28,12 @@ public class Fixture : IDisposable
Services = new ServiceCollection()
.AddMediator()
.AddLogging()
//.AddMidrandShopServices()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddMidrandShopDatabase(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.AddShopServices()
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
@@ -0,0 +1,10 @@
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
public class IntegrationFactAttribute : FactAttribute
{
public IntegrationFactAttribute()
{
if(!Debugger.IsAttached)
Skip = "This test requires the debugger to be attached.";
}
}
@@ -0,0 +1,201 @@
using LiteCharms.Features.MidrandBooks.Customers;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class CustomerServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly CustomerService customerService = fixture.Services.GetRequiredService<CustomerService>();
[IntegrationFact]
public async Task CreateCustomerAsync_ShouldReturn_ResultWithCustomerId()
{
var request = new CreateCustomer
{
Company = "Book Lovers",
Email = "hank@booklovers.com",
Phone = "555 1245 8577",
Website = "https://www.booklovers.com"
};
var result = await customerService.CreateCustomerAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task CreateCustomerContactAsync_ShouldReturn_ResultWithCustomerContactId()
{
var request = new CreateCustomerContact
{
Name = "Sipho",
LastName = "Madlanga",
Phone = "0710857365",
Email = "sipho@madlanga.africa",
Type = ContactTypes.Business
};
var result = await customerService.CreateCustomerContactAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task CreateCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddressId()
{
var request = new CreateCustomerAddress
{
Name = "Business",
BuildingType = AddressBuildingTypes.MixedUse,
Type = AddressType.Shipping,
Street = "123 Building 4, XYZ Suburb, Some Region",
City = "Johannesburg",
State = "Gauteng",
Country = "South Africa",
IsPrimary = true,
Enabled = true,
PostalCode = "12345"
};
var result = await customerService.CreateCustomerAddressAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateCustomerAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomer
{
Company = "Book Lovers",
Email = "hank@booklovers.com",
Phone = "555 1245 8578",
Website = "https://www.booklovers.com"
};
var result = await customerService.UpdateCustomerAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerContactAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomerContact
{
Name = "Sipho",
LastName = "Madlanga",
Phone = "0710857366",
Email = "sipho@madlanga.africa",
Type = ContactTypes.Business
};
var result = await customerService.UpdateCustomerContactAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerAddressAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomerAddress
{
Name = "Business",
BuildingType = AddressBuildingTypes.MixedUse,
Type = AddressType.Shipping,
Street = "123 Building 4, XYZ Suburb, Some Region",
City = "Johannesburg",
State = "Gauteng",
Country = "South Africa",
IsPrimary = true,
Enabled = true,
PostalCode = "12346"
};
var result = await customerService.UpdateCustomerAddressAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerContactStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerContactStatusAsync(1, true, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerAddressStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerAddressStatusAsync(1, true, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetCustomersAsync_ShouldReturn_ResultWithCustomerList()
{
var result = await customerService.GetCustomersAsync(fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerContactsAsync_ShouldReturn_ResultWithCustomerContactList()
{
var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAddressesAsync_ShouldReturn_ResultWithCustomerAddressList()
{
var result = await customerService.GetCustomerAddressesAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAsync_ShouldReturn_ResultWithCustomer()
{
var result = await customerService.GetCustomerAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetCustomerContactAsync_ShouldReturn_ResultWithCustomerContact()
{
var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddress()
{
var result = await customerService.GetCustomerAddressAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
}
@@ -39,6 +39,8 @@
</ItemGroup>
<ItemGroup>
<Using Include="System.Text.Json" />
<Using Include="System.Diagnostics" />
<Using Include="Xunit" />
</ItemGroup>
@@ -0,0 +1,196 @@
using LiteCharms.Features.MidrandBooks.Orders;
using LiteCharms.Features.MidrandBooks.Orders.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class OrderServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly OrderService orderService = fixture.Services.GetRequiredService<OrderService>();
[IntegrationFact]
public async Task CreateOrderAsync_ShouldReturn_ResultWithOrderId()
{
var request = new CreateOrder(250, "At the intercomm, dial 1 then option 2");
var result = await orderService.CreateOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task AddItemToOrderAsync_ShouldReturn_ResultWithOrderItemId()
{
var request = new CreateOrderItem(1, 1, 2);
var result = await orderService.AddItemToOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task AddItemsToOrderAsync_ShouldReturn_ResultWithSuccess()
{
var requests = new List<CreateOrderItem>
{
new(1, 1, 1),
new(1, 1, 3)
};
var result = await orderService.AddItemsToOrderAsync(1, [.. requests], fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task RemoveItemFromOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.RemoveItemFromOrderAsync(1, 5, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task ClearOrderItemsAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.ClearOrderItemsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CancelOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.CancelOrderAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetOrderAsync_ShouldReturn_ResultWithOrder()
{
var result = await orderService.GetOrderAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetOrdersByCustomerAsync_ShouldReturn_ResultWithOrderList()
{
var result = await orderService.GetOrdersByCustomerAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetOrdersAsync_ShouldReturn_ResultWithOrderList()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await orderService.GetOrdersAsync(range, 0, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task UpdateOrderStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateOrderStatusAsync(1, OrderStatus.Pending, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task AddShippingToOrderAsync_ShouldReturn_ResultWithSuccess()
{
var request = new CreateShipping(1, 2);
var result = await orderService.AddShippingToOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateShippingStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateShippingStatusAsync(1, ShippingStatuses.Shipped, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetShippingByOrderIdAsync_ShouldReturn_ResultWithShipping()
{
var result = await orderService.GetShippingByOrderIdAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task RemoveShippingFromOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.RemoveShippingFromOrderAsync(1, 1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateShippingTrackingNumberAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateShippingTrackingNumberAsync(1, 2, "NA0009969397");
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithShippingProviderId()
{
var request = new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel");
var result = await orderService.CreateShippingProviderAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task GetShippingProvidersAsync_ShouldReturn_ResultWithShippingProviderList()
{
var result = await orderService.GetShippingProvidersAsync(true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetShippingProviderAsync_ShouldReturn_ResultWithShippingProvider()
{
var result = await orderService.GetShippingProviderAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task UpdateShippingProviderAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateShippingProvider(2,true, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel");
var result = await orderService.UpdateShippingProviderAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -0,0 +1,63 @@
using LiteCharms.Features.MidrandBooks.Pages;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class PageServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly PageService pageService = fixture.Services.GetRequiredService<PageService>();
[IntegrationFact]
public async Task CreatePageAsync_ShouldReturn_ResultWithPageId()
{
}
[IntegrationFact]
public async Task GetPagesAsync_ByBookId_ShouldReturn_ResultWithPageList()
{
}
[IntegrationFact]
public async Task GetPageAsync_ShouldReturn_ResultWithPage()
{
}
[IntegrationFact]
public async Task GetPageByNumberAsync_ById_And_BookPageNumber_ShouldReturn_ResultWithPage()
{
}
[IntegrationFact]
public async Task UpdatePageAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeletePageAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeleteByPageTypeAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeleteAllAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task UpdatePageStatusAsync_ShouldReturn_ResultWithSuccess()
{
}
}
@@ -0,0 +1,143 @@
using LiteCharms.Features.MidrandBooks.Products;
using LiteCharms.Features.MidrandBooks.Products.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly ProductService productService = fixture.Services.GetRequiredService<ProductService>();
[IntegrationFact]
public async Task GetProductPriceAsync_ShouldReturn_RetultOneProductPrice()
{
var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductPricesAsync_ShouldReturn_RetultProductPriceList()
{
var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task SearchProductsAsync_ShouldReturn_RetultMatchingProducts()
{
var filter = new ProductFilter
{
Name = "system",
Manufacturer = "techwave",
SerialNumber = "2024",
MinPrice = 10,
MaxPrice = 30
};
var result = await productService.SearchProductsAsync(filter, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductAsync_ShouldReturn_RetultOneProduct()
{
var result = await productService.GetProductAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductsAsync_ShouldReturn_RetultProducts()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await productService.GetProductsAsync(0, range, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue()
{
var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateProductPriceStatusAsync_ShouldReturn_ResultTrue()
{
var result = await productService.UpdateProductPriceStatusAsync(2, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CreateProductPriceAsync_Should_Return_NewProductPriceId()
{
var request = new CreateProductPrice
{
Amount = 29.99m,
Discount = 0.00m
};
var result = await productService.CreateProductPriceAsync(2, request, fixture.CancellationToken);
Assert.True(result.IsSuccess, "Product price creation should be successful.");
Assert.True(result.Value > 0, "New ProductPriceId should be greater than 0.");
output.WriteLine($"Created ProductPriceId: {result.Value}");
}
[IntegrationFact]
public async Task CreateProductAsync_Result_Returns_ProductId()
{
var request = new CreateProduct
{
Name = "Systems Rewired",
Description = "[Design], <Code>, AND /CHAORS/ IN ***SYNC***",
Summary = "A comprehensive guide to systems thinking and design.",
ImageUrl = "https://bookshop.cdn.khongisa.co.za/design/2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp",
Type = ProductTypes.Book,
Categories = ["Systems Thinking", "Design", "Programming"],
Metadata = new ProductMetadata
{
CopyrightInfo = "© 2024 John Doe. All rights reserved.",
ManufactureDate = "2024-06-01",
Manufacturer = "TechWave Publishing",
SerialNumber = "SR-2024-0001"
}
};
var result = await productService.CreateProductAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess, "Product creation should be successful.");
Assert.True(result.Value > 0, "ProductId should be greater than 0.");
output.WriteLine($"Created ProductId: {result.Value}");
}
}
@@ -5,12 +5,6 @@
"BucketName": "bookshop",
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
},
"BookshopQuotesS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop.quotes",
"CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za"
},
"Email": {
"Credentials": {
"Username": "shop@litecharms.co.za"
@@ -7,23 +7,21 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks;
public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result> UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken)
public async ValueTask<Result> UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var book = await context.Books.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken);
var rowsUpdated = await context.Books
.Where(b => b.Id == bookId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.Enabled, isEnabled)
.SetProperty(b => b.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (book is null)
return Result.Fail(new Error($"Book with ID {bookId} not found"));
book.UpdatedAt = DateTime.UtcNow;
book.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to change status of book with ID {bookId}"));
: Result.Fail(new Error($"Book with ID {bookId} not found"));
}
catch (Exception ex)
{
@@ -45,8 +43,9 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
var book = context.Books.Add(new Entities.AuthorBook
{
CreatedAt = DateTime.UtcNow,
AuthorId = authorId,
ProductId = productId,
ProductId = productId
});
return await context.SaveChangesAsync(cancellationToken) > 0
@@ -68,7 +67,8 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
var book = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.Include(b => b.Product)
.ThenInclude(b => b!.Prices)
.Include(b => b.Pages)
.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken);
@@ -86,7 +86,7 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken))
return Result.Fail<AuthorBook[]>(new Error($"Author with ID {authorId} not found"));
@@ -94,7 +94,8 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
var books = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.Include(b => b.Product)
.ThenInclude(b => b.Prices)
.OrderByDescending(b => b.CreatedAt)
.Where(b => b.AuthorId == authorId)
.ToListAsync(cancellationToken);
@@ -118,9 +119,10 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
var books = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.Include(b => b.Product)
.ThenInclude(b => b!.Prices)
.Include(b => b.Pages)
.Where(b => b.Enabled && b.Product!.Enabled && b.Author.Enabled)
.Where(b => b.Enabled && b.Product!.Enabled && b.Author!.Enabled)
.OrderByDescending(b => b.Ranking)
.ThenByDescending(b => b.Ranking)
.ThenByDescending(b => b.CreatedAt)
@@ -6,7 +6,7 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
public class AuthorBook : Models.AuthorBook
{
public virtual Author Author { get; set; } = new();
public virtual Author? Author { get; set; }
public new virtual Product? Product { get; set; }
@@ -1,5 +1,4 @@
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres;
@@ -9,53 +8,21 @@ namespace LiteCharms.Features.MidrandBooks.Authors;
public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result<AuthorBook[]>> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken)
public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
var rowsUpdated = await context.Authors
.Where(a => a.Id == authorId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(a => a.Enabled, isEnabled)
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (author is null)
return Result.Fail<AuthorBook[]>(new Error($"Author with ID {authorId} not found"));
var books = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.OrderByDescending(b => b.CreatedAt)
.Where(p => p.AuthorId == authorId)
.AsSplitQuery()
.ToArrayAsync(cancellationToken);
return books?.Length > 0
? Result.Ok(books.Select(b => b.ToModel()).ToArray())
: Result.Fail<AuthorBook[]>(new Error($"No books found for author with ID {authorId}"));
}
catch (Exception ex)
{
return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
if (author is null)
return Result.Fail(new Error($"Author with ID {authorId} not found"));
author.UpdatedAt = DateTime.UtcNow;
author.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to change status of author with ID {authorId}"));
: Result.Fail(new Error($"Author with ID {authorId} not found"));
}
catch (Exception ex)
{
@@ -81,12 +48,12 @@ public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> conte
}
}
public async ValueTask<Result<Author[]>> GetAuthors(DateRange range, CancellationToken cancellationToken)
public async ValueTask<Result<Author[]>> GetAuthorsAsync(DateRange range, CancellationToken cancellationToken = default)
{
try
{
var fromDate = range.From.ToDateTime(TimeOnly.MinValue);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue);
var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -107,18 +74,12 @@ public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> conte
}
}
public async ValueTask<Result> UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken)
public async ValueTask<Result> UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken))
return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists"));
if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken))
return Result.Fail(new Error($"An author with the email {request.Email} already exists"));
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
if (author is null)
@@ -151,12 +112,12 @@ public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> conte
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if(await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken))
if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken))
return Result.Fail<long>(new Error($"An author with the name {request.Name} {request.LastName} already exists"));
if(await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken))
if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken))
return Result.Fail<long>(new Error($"An author with the email {request.Email} already exists"));
var newAuthor = context.Authors.Add(new Entities.Author
@@ -18,7 +18,8 @@ public sealed class AuthorConfiguration : IEntityTypeConfiguration<Author>
builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048);
builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048);
builder.Property(f => f.SocialMedia).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Enabled).HasDefaultValue(true);
builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); });
}
}
@@ -30,7 +30,7 @@ public class Author
public string? ThumbnailImageUrl { get; set; }
public SocialMedia[]? SocialMedia { get; set; }
public ICollection<SocialMedia>? SocialMedia { get; set; }
public bool Enabled { get; set; }
}
@@ -13,7 +13,7 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Customers.AnyAsync(c => c.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken))
if (await context.Customers.AnyAsync(c => EF.Functions.ILike(c.Email!, $"%{request.Email}%"), cancellationToken))
return Result.Fail<long>(new Error($"Customer with email '{request.Email}' already exists."));
var customer = context.Customers.Add(new Entities.Customer
@@ -46,7 +46,7 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
return Result.Fail<long>(new Error($"Customer with ID '{customerId}' does not exist."));
if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && cc.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken))
if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && EF.Functions.ILike(cc.Email!, $"%{request.Email}%"), cancellationToken))
return Result.Fail<long>(new Error($"Contact with email '{request.Email}' already exists for this customer."));
var contact = context.Contacts.Add(new Entities.Contact
@@ -139,21 +139,19 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken);
var rowsUpdated = await context.Contacts
.Where(cc => cc.Id == contactId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(cc => cc.Name, request.Name)
.SetProperty(cc => cc.LastName, request.LastName)
.SetProperty(cc => cc.Email, request.Email)
.SetProperty(cc => cc.Phone, request.Phone)
.SetProperty(cc => cc.Type, request.Type)
.SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (contact is null)
return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
contact.UpdatedAt = DateTime.UtcNow;
contact.Name = request.Name;
contact.LastName = request.LastName;
contact.Email = request.Email;
contact.Phone = request.Phone;
contact.Type = request.Type;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer contact."));
: Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
}
catch (Exception ex)
{
@@ -167,25 +165,23 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken);
var rowsUpdated = await context.Addresses
.Where(a => a.Id == addressId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(a => a.Street, request.Street)
.SetProperty(a => a.City, request.City)
.SetProperty(a => a.State, request.State)
.SetProperty(a => a.PostalCode, request.PostalCode)
.SetProperty(a => a.Country, request.Country)
.SetProperty(a => a.Type, request.Type)
.SetProperty(a => a.BuildingType, request.BuildingType)
.SetProperty(a => a.IsPrimary, request.IsPrimary)
.SetProperty(a => a.Name, request.Name)
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (address is null)
return Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
address.UpdatedAt = DateTime.UtcNow;
address.Street = request.Street;
address.City = request.City;
address.State = request.State;
address.PostalCode = request.PostalCode;
address.Country = request.Country;
address.Type = request.Type;
address.BuildingType = request.BuildingType;
address.IsPrimary = request.IsPrimary;
address.Name = request.Name;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer address."));
: Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
}
catch (Exception ex)
{
@@ -199,17 +195,15 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken);
var rowsUpdated = await context.Customers
.Where(c => c.Id == customerId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(c => c.Enabled, enabled)
.SetProperty(c => c.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (customer is null)
return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist."));
customer.Enabled = enabled;
customer.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer status."));
: Result.Fail(new Error($"Customer with ID '{customerId}' does not exist."));
}
catch (Exception ex)
{
@@ -223,18 +217,16 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken);
var rowsUpdated = await context.Contacts
.Where(cc => cc.Id == contactId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(cc => cc.Enabled, enabled)
.SetProperty(cc => cc.IsPrimary, isPrimary)
.SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (contact is null)
return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
contact.Enabled = enabled;
contact.IsPrimary = isPrimary;
contact.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer contact status."));
: Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
}
catch (Exception ex)
{
@@ -248,18 +240,16 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken);
var rowsUpdated = await context.Addresses
.Where(a => a.Id == addressId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(a => a.Enabled, enabled)
.SetProperty(a => a.IsPrimary, isPrimary)
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (address is null)
return Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
address.Enabled = enabled;
address.IsPrimary = isPrimary;
address.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer address status."));
: Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
}
catch (Exception ex)
{
@@ -14,7 +14,8 @@ public sealed class CustomerConfiguration : IEntityTypeConfiguration<Customer>
builder.Property(c => c.Email).IsRequired();
builder.Property(c => c.Phone).IsRequired();
builder.Property(c => c.Website).IsRequired();
builder.Property(c => c.SocialMedia).IsRequired(false).HasColumnType("jsonb");
builder.Property(c => c.Enabled).HasDefaultValue(true);
builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); });
}
}
@@ -20,7 +20,7 @@ public class Customer
public string? Phone { get; set; }
public SocialMedia[]? SocialMedia { get; set; }
public ICollection<SocialMedia>? SocialMedia { get; set; }
public bool Enabled { get; set; }
}
@@ -14,7 +14,7 @@ public record CreateCustomer
public string? Phone { get; set; }
public SocialMedia[]? SocialMedia { get; set; }
public ICollection<SocialMedia>? SocialMedia { get; set; }
}
public sealed record UpdateCustomer : CreateCustomer;
@@ -17,7 +17,8 @@ public static class Mappers
Name = entity.Name,
Type = entity.Type,
Price = entity.Price,
Enabled = entity.Enabled
Enabled = entity.Enabled,
TrackingUrl = entity.TrackingUrl,
};
public static Shipping ToModel(this Orders.Entities.Shipping entity) => new()
@@ -8,8 +8,18 @@ public static class Postgres
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName);
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
dataSourceBuilder.ConfigureTypeLoading(options => { options.EnableTypeLoading(false); });
var dataSource = dataSourceBuilder.Build();
services.AddSingleton(dataSource);
services.AddPooledDbContextFactory<MidrandBooksDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName)));
options.UseNpgsql(dataSource));
return services;
}
@@ -4,19 +4,15 @@ namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class Shop
{
public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime)
public static IServiceCollection AddShopServices(this IServiceCollection services)
{
var serviceType = typeof(IService);
var implementations = assembly.GetTypes()
var implementations = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
foreach (var implementation in implementations)
{
var descriptor = new ServiceDescriptor(serviceType, implementation, serviceLifetime);
services.Add(descriptor);
}
foreach (var implementation in implementations)
services.AddScoped(implementation);
return services;
}
@@ -11,7 +11,7 @@ public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(o => o.Status).IsRequired();
builder.Property(o => o.Total).IsRequired().HasColumnType("decimal(18,2)");
builder.Property(o => o.Total).IsRequired().HasPrecision(18, 2);
builder.Property(o => o.Notes).HasMaxLength(1000);
}
}
@@ -1,10 +1,10 @@
namespace LiteCharms.Features.MidrandBooks.Orders.Models;
public sealed record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes);
public sealed record CreateOrder(decimal TotalPrice, string? Notes);
public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity);
public sealed record CreateShipping(long OrderId, long AddressId, long ShippingProviderId, string? TrackingNumber);
public sealed record CreateShipping(long AddressId, long ShippingProviderId, string? TrackingNumber = null);
public sealed record CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl);
@@ -49,6 +49,17 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken))
return Result.Fail<long>("Product price not found.");
var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == request.ProductPriceId && i.OrderId == orderId, cancellationToken);
if(existingItem is not null)
{
existingItem.Quantity += request.Quantity;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(existingItem.Id)
: Result.Fail<long>("Update existing order item.");
}
var orderItem = context.OrderItems.Add(new Entities.OrderItem
{
OrderId = orderId,
@@ -78,9 +89,6 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken))
return Result.Fail("Order not found.");
var existingItems = context.OrderItems.Where(oi => oi.OrderId == orderId);
context.OrderItems.RemoveRange(existingItems);
foreach (var item in items)
{
@@ -90,18 +98,23 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken))
return Result.Fail($"Product price with ID {item.ProductPriceId} not found.");
context.OrderItems.Add(new Entities.OrderItem
{
OrderId = orderId,
AuthorBookId = item.AuthorBookId,
ProductPriceId = item.ProductPriceId,
Quantity = item.Quantity
});
var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == item.ProductPriceId && i.OrderId == orderId, cancellationToken);
if (existingItem is not null)
existingItem.Quantity += item.Quantity;
else
context.OrderItems.Add(new Entities.OrderItem
{
OrderId = orderId,
AuthorBookId = item.AuthorBookId,
ProductPriceId = item.ProductPriceId,
Quantity = item.Quantity
});
await context.SaveChangesAsync(cancellationToken);
}
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to add items to order.");
return Result.Ok();
}
catch (Exception ex)
{
@@ -115,16 +128,13 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var orderItem = await context.OrderItems.FirstOrDefaultAsync(oi => oi.Id == orderItemId && oi.OrderId == orderId, cancellationToken);
var rowsDeleted = await context.OrderItems
.Where(oi => oi.Id == orderItemId && oi.OrderId == orderId)
.ExecuteDeleteAsync(cancellationToken);
if (orderItem is null)
return Result.Fail("Order item not found.");
context.OrderItems.Remove(orderItem);
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsDeleted > 0
? Result.Ok()
: Result.Fail("Failed to remove item from order.");
: Result.Fail("Order item not found or failed to remove.");
}
catch (Exception ex)
{
@@ -132,15 +142,14 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
}
}
public async ValueTask<Result> ClearOrderItemasAsync(long orderId, CancellationToken cancellationToken = default)
public async ValueTask<Result> ClearOrderItemsAsync(long orderId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var orderItems = context.OrderItems.Where(oi => oi.OrderId == orderId);
context.OrderItems.RemoveRange(orderItems);
var deletedItems = await context.OrderItems.Where(oi => oi.OrderId == orderId)
.ExecuteDeleteAsync(cancellationToken);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
@@ -199,12 +208,15 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{
try
{
var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var orders = await context.Orders
.AsNoTracking()
.Where(o => o.CreatedAt >= range.From.ToDateTime(TimeOnly.MinValue) && o.CreatedAt <= range.To.ToDateTime(TimeOnly.MaxValue))
.Skip(index * range.MaxRecords)
.Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate)
.Skip(index).Take(range.MaxRecords)
.ToListAsync(cancellationToken);
return Result.Ok(orders.Select(o => o.ToModel()).ToArray());
@@ -221,17 +233,15 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
var rowsUpdated = await context.Orders
.Where(o => o.Id == orderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(o => o.Status, newStatus)
.SetProperty(o => o.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (order is null)
return Result.Fail("Order not found.");
order.UpdatedAt = DateTime.UtcNow;
order.Status = newStatus;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail("Failed to update order status.");
: Result.Fail("Order not found or status update failed.");
}
catch (Exception ex)
{
@@ -239,7 +249,7 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
}
}
public async ValueTask<Result> AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default)
public async ValueTask<Result<long>> AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default)
{
try
{
@@ -282,17 +292,16 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken);
var rowsUpdated = await context.Shippings
.Where(s => s.OrderId == orderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(s => s.Status, newStatus)
.SetProperty(s => s.UpdatedAt, DateTime.UtcNow),
cancellationToken);
if (shipping is null)
return Result.Fail("Shipping not found for this order.");
shipping.UpdatedAt = DateTime.UtcNow;
shipping.Status = newStatus;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail("Failed to update shipping status.");
: Result.Fail("Shipping not found for this order or status update failed.");
}
catch (Exception ex)
{
@@ -325,21 +334,14 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rowsDeleted = await context.Shippings
.Where(s => s.Id == shippingId && s.OrderId == orderId)
.ExecuteDeleteAsync(cancellationToken);
if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken))
return Result.Fail("Order not found.");
var shipping = await context.Shippings.AsNoTracking()
.FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken);
if (shipping is null)
return Result.Fail("Shipping not found for this order.");
context.Shippings.Remove(shipping);
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsDeleted > 0
? Result.Ok()
: Result.Fail("Failed to remove shipping from order.");
: Result.Fail("Shipping record not found for this order.");
}
catch (Exception ex)
{
@@ -353,17 +355,15 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken);
var rowsUpdated = await context.Shippings
.Where(s => s.Id == shippingId && s.OrderId == orderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(s => s.TrackingNumber, trackingNumber)
.SetProperty(s => s.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (shipping is null)
return Result.Fail("Shipping not found for this order.");
shipping.UpdatedAt = DateTime.UtcNow;
shipping.TrackingNumber = trackingNumber;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail("Failed to update shipping tracking number.");
: Result.Fail("Shipping record not found for this order.");
}
catch (Exception ex)
{
@@ -371,7 +371,7 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
}
}
public async ValueTask<Result> CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default)
public async ValueTask<Result<long>> CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default)
{
try
{
@@ -382,6 +382,7 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider
{
CreatedAt = DateTime.UtcNow,
Name = request.Name,
Type = request.Type,
Price = request.Price,
@@ -389,7 +390,7 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
? Result.Ok(shippingProvider.Entity.Id)
: Result.Fail("Failed to create shipping provider.");
}
catch (Exception ex)
@@ -440,24 +441,22 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var provider = await context.ShippingProviders.FirstOrDefaultAsync(sp => sp.Id == request.ProviderId, cancellationToken);
var rowsUpdated = await context.ShippingProviders
.Where(sp => sp.Id == request.ProviderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(sp => sp.Name, request.Name)
.SetProperty(sp => sp.Price, request.Price)
.SetProperty(sp => sp.TrackingUrl, request.TrackingUrl)
.SetProperty(sp => sp.Enabled, request.Enabled)
.SetProperty(sp => sp.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (provider is null)
return Result.Fail("Shipping provider not found.");
provider.UpdatedAt = DateTime.UtcNow;
provider.Enabled = request.Enabled;
provider.Name = request.Name;
provider.Price = request.Price;
provider.TrackingUrl = request.TrackingUrl;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail("Failed to update shipping provider status.");
: Result.Fail("Shipping provider not found.");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
}
}
@@ -13,10 +13,11 @@ public sealed class BookPageConfiguration : IEntityTypeConfiguration<BookPage>
builder.Property(bp => bp.AuthorBookId).IsRequired();
builder.Property(bp => bp.Content).IsRequired();
builder.Property(bp => bp.Type).IsRequired();
builder.Property(bp => bp.ContentType).IsRequired();
builder.Property(bp => bp.Notes).IsRequired(false).HasColumnType("jsonb");
builder.Property(bp => bp.References).IsRequired(false).HasColumnType("jsonb");
builder.Property(bp => bp.ContentType).IsRequired();
builder.Property(bp => bp.Enabled).HasDefaultValue(true);
builder.Property(bp => bp.Notes).IsRequired(false);
builder.OwnsMany(f => f.References, b => { b.ToJson(); });
builder.HasOne(f =>f.Book)
.WithMany(b => b.Pages)
@@ -22,7 +22,7 @@ public class BookPage
public string[]? Notes { get; set; }
public PageReference[]? References { get; set; }
public ICollection<PageReference>? References { get; set; }
public bool Enabled { get; set; }
}
@@ -14,5 +14,7 @@ public class CreateBookPage
public string[]? Notes { get; set; }
public PageReference[]? References { get; set; }
public ICollection<PageReference>? References { get; set; }
}
public sealed class UpdateBookPage : CreateBookPage;
@@ -1,3 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
public sealed class UpdateBookPage : CreateBookPage;
@@ -13,19 +13,13 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken))
return Result.Fail("Book not found");
var rowsDeleted = await context.Pages
.Where(p => p.AuthorBookId == authorBookId)
.ExecuteDeleteAsync(cancellationToken);
var pages = await context.Pages.Where(p => p.AuthorBookId == authorBookId).ToListAsync(cancellationToken);
if (pages.Count == 0)
return Result.Fail("No pages found for the specified book");
context.Pages.RemoveRange(pages);
await context.SaveChangesAsync(cancellationToken);
return Result.Ok();
return rowsDeleted > 0
? Result.Ok()
: Result.Fail("No pages found for the specified book");
}
catch (Exception ex)
{
@@ -39,16 +33,13 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType, cancellationToken);
var rowsDeleted = await context.Pages
.Where(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType)
.ExecuteDeleteAsync(cancellationToken);
if (page is null)
return Result.Fail("Page not found");
context.Pages.Remove(page);
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsDeleted > 0
? Result.Ok()
: Result.Fail("Failed to delete page");
: Result.Fail("Page not found");
}
catch (Exception ex)
{
@@ -62,17 +53,15 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken);
var rowsUpdated = await context.Pages
.Where(p => p.Id == bookPageId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Enabled, enabled)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (page is null)
return Result.Fail("Page not found");
page.UpdatedAt = DateTime.UtcNow;
page.Enabled = enabled;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail("Failed to update page status");
: Result.Fail("Page not found");
}
catch (Exception ex)
{
@@ -86,16 +75,13 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken);
var rowsDeleted = await context.Pages
.Where(p => p.Id == bookPageId)
.ExecuteDeleteAsync(cancellationToken);
if (page is null)
return Result.Fail("Page not found");
context.Pages.Remove(page);
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsDeleted > 0
? Result.Ok()
: Result.Fail("Failed to delete page");
: Result.Fail("Page not found");
}
catch (Exception ex)
{
@@ -109,22 +95,20 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken);
var rowsUpdated = await context.Pages
.Where(p => p.Id == bookPageId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Type, request.Type)
.SetProperty(p => p.ContentType, request.ContentType)
.SetProperty(p => p.Number, request.Number)
.SetProperty(p => p.Content, request.Content)
.SetProperty(p => p.Notes, request.Notes)
.SetProperty(p => p.References, request.References)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (page is null)
return Result.Fail("Page not found");
page.UpdatedAt = DateTime.UtcNow;
page.Type = request.Type;
page.ContentType = request.ContentType;
page.Number = request.Number;
page.Content = request.Content;
page.Notes = request.Notes;
page.References = request.References;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail("Failed to update page");
: Result.Fail("Page not found");
}
catch (Exception ex)
{
@@ -1,7 +1,6 @@
// <auto-generated />
using System;
using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -13,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{
[DbContext(typeof(MidrandBooksDbContext))]
[Migration("20260527070840_Init")]
[Migration("20260529070104_Init")]
partial class Init
{
/// <inheritdoc />
@@ -112,9 +111,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Property<int>("PublisherType")
.HasColumnType("integer");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<string>("ThumbnailImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
@@ -291,9 +287,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
@@ -338,7 +331,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("integer");
b.Property<decimal>("Total")
.HasColumnType("decimal(18,2)");
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
@@ -494,17 +488,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("boolean")
.HasDefaultValue(true);
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string[]>("Notes")
.HasColumnType("text[]");
b.Property<int>("Number")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<PageReference[]>("References")
.HasColumnType("jsonb");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -570,8 +561,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.PrimitiveCollection<string>("Categories")
.HasColumnType("jsonb");
b.PrimitiveCollection<string[]>("Categories")
.HasColumnType("text[]");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
@@ -591,9 +582,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
@@ -604,8 +592,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("ThumbnailUrls")
.HasColumnType("jsonb");
b.PrimitiveCollection<string[]>("ThumbnailUrls")
.HasColumnType("text[]");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -661,40 +649,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.ToTable("Prices", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Discount")
.HasColumnType("numeric");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<long>("ProductId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId")
.IsUnique();
b.ToTable("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author")
@@ -714,6 +668,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
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")
@@ -736,6 +722,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
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")
@@ -798,7 +816,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.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 =>
@@ -812,6 +857,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
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")
@@ -823,15 +897,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null)
.WithOne("Price")
.HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{
b.Navigation("Pages");
@@ -865,8 +930,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{
b.Navigation("Price");
b.Navigation("Prices");
});
#pragma warning restore 612, 618
@@ -1,5 +1,4 @@
using System;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
@@ -31,8 +30,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Website = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
ThumbnailImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
SocialMedia = table.Column<SocialMedia[]>(type: "jsonb", nullable: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
SocialMedia = table.Column<string>(type: "jsonb", nullable: true)
},
constraints: table =>
{
@@ -52,8 +51,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Email = table.Column<string>(type: "text", nullable: false),
Website = table.Column<string>(type: "text", nullable: false),
Phone = table.Column<string>(type: "text", nullable: false),
SocialMedia = table.Column<SocialMedia[]>(type: "jsonb", nullable: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
SocialMedia = table.Column<string>(type: "jsonb", nullable: true)
},
constraints: table =>
{
@@ -70,7 +69,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
CustomerId = table.Column<long>(type: "bigint", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
Total = table.Column<decimal>(type: "numeric(18,2)", nullable: false),
Total = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Notes = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
InvoiceUrl = table.Column<string>(type: "text", nullable: true)
},
@@ -92,10 +91,10 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ImageUrl = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ThumbnailUrls = table.Column<string>(type: "jsonb", nullable: true),
Categories = table.Column<string>(type: "jsonb", nullable: true),
Metadata = table.Column<ProductMetadata>(type: "jsonb", nullable: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
ThumbnailUrls = table.Column<string[]>(type: "text[]", nullable: true),
Categories = table.Column<string[]>(type: "text[]", nullable: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
Metadata = table.Column<string>(type: "jsonb", nullable: true)
},
constraints: table =>
{
@@ -260,30 +259,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "ProductPrice",
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),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ProductId = table.Column<long>(type: "bigint", nullable: false),
Amount = table.Column<decimal>(type: "numeric", nullable: false),
Discount = table.Column<decimal>(type: "numeric", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProductPrice", x => x.Id);
table.ForeignKey(
name: "FK_ProductPrice_Products_ProductId",
column: x => x.ProductId,
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Shippings",
columns: table => new
@@ -334,9 +309,9 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
ContentType = table.Column<int>(type: "integer", nullable: false),
Number = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
Content = table.Column<byte[]>(type: "bytea", nullable: false),
Notes = table.Column<string>(type: "jsonb", nullable: true),
References = table.Column<PageReference[]>(type: "jsonb", nullable: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
Notes = table.Column<string[]>(type: "text[]", nullable: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
References = table.Column<string>(type: "jsonb", nullable: true)
},
constraints: table =>
{
@@ -428,12 +403,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
table: "Prices",
column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_ProductPrice_ProductId",
table: "ProductPrice",
column: "ProductId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Refunds_OrderId",
table: "Refunds",
@@ -468,9 +437,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
migrationBuilder.DropTable(
name: "OrderItems");
migrationBuilder.DropTable(
name: "ProductPrice");
migrationBuilder.DropTable(
name: "Refunds");
@@ -1,7 +1,6 @@
// <auto-generated />
using System;
using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -109,9 +108,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Property<int>("PublisherType")
.HasColumnType("integer");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<string>("ThumbnailImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
@@ -288,9 +284,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
@@ -335,7 +328,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("integer");
b.Property<decimal>("Total")
.HasColumnType("decimal(18,2)");
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd()
@@ -491,17 +485,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("boolean")
.HasDefaultValue(true);
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string[]>("Notes")
.HasColumnType("text[]");
b.Property<int>("Number")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(0);
b.Property<PageReference[]>("References")
.HasColumnType("jsonb");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -567,8 +558,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.PrimitiveCollection<string>("Categories")
.HasColumnType("jsonb");
b.PrimitiveCollection<string[]>("Categories")
.HasColumnType("text[]");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
@@ -588,9 +579,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(255)
@@ -601,8 +589,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("ThumbnailUrls")
.HasColumnType("jsonb");
b.PrimitiveCollection<string[]>("ThumbnailUrls")
.HasColumnType("text[]");
b.Property<int>("Type")
.HasColumnType("integer");
@@ -658,40 +646,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.ToTable("Prices", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Discount")
.HasColumnType("numeric");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<long>("ProductId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId")
.IsUnique();
b.ToTable("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author")
@@ -711,6 +665,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
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")
@@ -733,6 +719,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
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")
@@ -795,7 +813,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.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 =>
@@ -809,6 +854,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
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")
@@ -820,15 +894,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null)
.WithOne("Price")
.HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{
b.Navigation("Pages");
@@ -862,8 +927,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{
b.Navigation("Price");
b.Navigation("Prices");
});
#pragma warning restore 612, 618
@@ -4,6 +4,8 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.Ignore(p => p.Price);
builder.ToTable("Products");
builder.HasKey(f => f.Id);
@@ -14,9 +16,10 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
builder.Property(f => f.Description).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).HasMaxLength(1024);
builder.Property(f => f.ThumbnailUrls).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Metadata).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Categories).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Enabled).HasDefaultValue(false);
builder.Property(f => f.Categories).IsRequired(false);
builder.Property(f => f.ThumbnailUrls).IsRequired(false);
builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); });
}
}
@@ -3,5 +3,5 @@
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
public class ProductPrice : Models.ProductPrice
{
public virtual Product Product { get; set; } = new();
public virtual Product? Product { get; set; }
}
@@ -1,10 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public sealed class CreateProductPrice
{
public long ProductId { get; set; }
public decimal Amount { get; set; }
public decimal Discount { get; set; }
}
@@ -20,3 +20,10 @@ public sealed record CreateProduct
public ProductMetadata? Metadata { get; set; }
}
public sealed class CreateProductPrice
{
public decimal Amount { get; set; }
public decimal Discount { get; set; }
}
@@ -14,17 +14,15 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var productPrice = await context.Prices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
var rowsUpdated = await context.Prices
.Where(p => p.Id == productPriceId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Enabled, isEnabled)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (productPrice is null)
return Result.Fail(new Error($"Product price with ID {productPriceId} not found"));
productPrice.UpdatedAt = DateTime.UtcNow;
productPrice.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to change status of product price with ID {productPriceId}"));
: Result.Fail(new Error($"Product price with ID {productPriceId} not found"));
}
catch (Exception ex)
{
@@ -38,17 +36,15 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
var rowsUpdated = await context.Products
.Where(p => p.Id == productId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Enabled, isEnabled)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (product is null)
return Result.Fail(new Error($"Product with ID {productId} not found"));
product.UpdatedAt = DateTime.UtcNow;
product.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to change status of product with ID {productId}"));
: Result.Fail(new Error($"Product with ID {productId} not found"));
}
catch (Exception ex)
{
@@ -64,17 +60,22 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
var query = context.Products.AsQueryable();
var cultureInfo = CultureInfo.InvariantCulture;
if (!string.IsNullOrWhiteSpace(filter.Name))
query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Name}%"));
if (!string.IsNullOrWhiteSpace(filter.Title))
query = query.Where(p => p.Name!.Contains(filter.Title));
query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%"));
if (!string.IsNullOrWhiteSpace(filter.Category))
query = query.Where(p => p.Categories!.Any(c => c == filter.Category));
query = query.Where(p => p.Categories.Contains(filter.Category));
if (!string.IsNullOrWhiteSpace(filter.Manufacturer))
query = query.Where(p => p.Metadata!.Manufacturer == filter.Manufacturer);
query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%"));
if (!string.IsNullOrWhiteSpace(filter.SerialNumber))
query = query.Where(p => p.Metadata!.SerialNumber == filter.SerialNumber);
query = query.Where(p => EF.Functions.ILike(p.Metadata!.SerialNumber!, $"%{filter.SerialNumber}%"));
if (filter.MinPrice > 0)
query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice));
@@ -165,7 +166,7 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
}
}
public async ValueTask<Result<ProductPrice[]>> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default)
public async ValueTask<Result<ProductPrice>> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
@@ -175,16 +176,16 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
.AsNoTracking()
.OrderByDescending(p => p.CreatedAt)
.ThenBy(p => p.UpdatedAt)
.FirstOrDefaultAsync(p => p.ProductId == productId, cancellationToken);
.FirstOrDefaultAsync(p => p.ProductId == productId && p.Enabled, cancellationToken);
return product is not null
? Result.Ok(new[] { product.ToModel() })
: Result.Fail<ProductPrice[]>(new Error($"No price found for product with ID {productId}"));
? Result.Ok(product.ToModel())
: Result.Fail<ProductPrice>(new Error($"No price found for product with ID {productId}"));
}
catch (Exception ex)
{
return Result.Fail<ProductPrice[]>(new Error(ex.Message).CausedBy(ex));
return Result.Fail<ProductPrice>(new Error(ex.Message).CausedBy(ex));
}
}
@@ -228,8 +229,8 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{
try
{
var fromDate = range.From.ToDateTime(TimeOnly.MinValue);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue);
var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -244,7 +245,7 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
.AsSplitQuery()
.ToArrayAsync(cancellationToken);
return await context.SaveChangesAsync(cancellationToken) > 0
return products?.Length > 0
? Result.Ok(products.Select(p => p.ToModel()).ToArray())
: Result.Fail<Product[]>(new Error("Failed to retrieve products."));
}
+2 -1
View File
@@ -93,7 +93,8 @@ public enum SocialMediaTypes : int
YouTube = 5,
Pinterest = 6,
Reddit = 7,
Tumblr = 8
Tumblr = 8,
GitHub = 9
}
public enum EmailStatuses : int
+1
View File
@@ -9,6 +9,7 @@
<Project Path="LiteCharms.Features/LiteCharms.Features.csproj" />
</Folder>
<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.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" />