Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f44acd8c82 |
+1
-5
@@ -19,10 +19,6 @@ steps:
|
||||
commands:
|
||||
- dotnet pack LiteCharms.Features/LiteCharms.Features.csproj -c Release -p:PackageVersion=$VERSION -o dist/
|
||||
- dotnet nuget push dist/LiteCharms.Features.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
|
||||
- dotnet pack LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj -c Release -p:PackageVersion=$VERSION -o dist/
|
||||
- dotnet nuget push dist/LiteCharms.Features.TechShop.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
|
||||
- dotnet pack LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj -c Release -p:PackageVersion=$VERSION -o dist/
|
||||
- dotnet nuget push dist/LiteCharms.Features.MidrandBooks.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
|
||||
|
||||
- name: gitea-tag-release
|
||||
image: alpine/git
|
||||
@@ -45,7 +41,7 @@ steps:
|
||||
\"tag_name\": \"$VERSION\",
|
||||
\"target_commitish\": \"${DRONE_COMMIT_SHA}\",
|
||||
\"name\": \"Library Suite $VERSION\",
|
||||
\"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n* LiteCharms.Features.TechShop\n* LiteCharms.Features.MidrandBooks\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
|
||||
\"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
|
||||
\"draft\": false,
|
||||
\"prerelease\": false
|
||||
}"
|
||||
|
||||
@@ -3,45 +3,6 @@ root = true
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
# IDE0250: Prefer make struct 'readonly'
|
||||
dotnet_diagnostic.IDE0250.severity = warning
|
||||
|
||||
# IDE0060: Remove unused parameters (Good cleanup pairing)
|
||||
dotnet_diagnostic.IDE0060.severity = warning
|
||||
|
||||
# CA1852: Seal internal types (Available in modern .NET)
|
||||
dotnet_diagnostic.CA1852.severity = warning
|
||||
|
||||
# MA0018: Add sealed modifier to types that are never inherited
|
||||
dotnet_diagnostic.MA0018.severity = warning
|
||||
|
||||
# Enforce that classes should be sealed
|
||||
dotnet_diagnostic.MA0053.severity = warning
|
||||
|
||||
# CRITICAL: Force the analyzer to also flag PUBLIC classes, not just internal ones
|
||||
meziantou_analyzer.MA0053.public_class_should_be_sealed = true
|
||||
MA0053.public_class_should_be_sealed = true
|
||||
|
||||
# Keep the rule active as a warning by default
|
||||
dotnet_diagnostic.MA0048.severity = warning
|
||||
|
||||
# Specific exclusions for Meziantou.Analyzer MA0048
|
||||
# Disable the rule for enums
|
||||
meziantou_analyzer.MA0048.exclude_enums = true
|
||||
|
||||
# Disable the rule for records
|
||||
meziantou_analyzer.MA0048.exclude_records = true
|
||||
|
||||
#EXCLUDE specific files that are meant to hold grouped enums/records
|
||||
dotnet_diagnostic.MA0048.severity = warning
|
||||
|
||||
# Disable the requirement to specify ConfigureAwait(false)
|
||||
dotnet_diagnostic.MA0004.severity = none
|
||||
|
||||
# ALTERNATIVE: Exclude any file ending with 'Enums.cs' or 'Records.cs'
|
||||
# (e.g., BillingEnums.cs, CustomerRecords.cs)
|
||||
[**/*{Enums,Records}.cs]
|
||||
dotnet_diagnostic.MA0048.severity = none
|
||||
|
||||
#### Core EditorConfig Options ####
|
||||
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Categories;
|
||||
using LiteCharms.Features.MidrandBooks.Products;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||
|
||||
public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
||||
ILogger<CategorySeederService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (!await features.IsEnabledAsync("CategorySeederService")) return;
|
||||
|
||||
logger.LogInformation("Category and Product-Tag Mapping Seeding started (15-char limit applied)");
|
||||
|
||||
// Initialize Bogus to ensure repeatable distribution matrix pathing
|
||||
var faker = new Faker();
|
||||
Randomizer.Seed = new Random(101);
|
||||
|
||||
// 1. Curate Broad Book Categories (IsMain = true, Max 20, Max 15 chars)
|
||||
var broadMainCategories = new[]
|
||||
{
|
||||
"Fiction", "Non-Fiction", "Youth & Kids", "Academic",
|
||||
"Biographies", "Business", "Sci-Fi & Fantasy",
|
||||
"Thrillers", "Self-Help", "History",
|
||||
"Spirituality", "Arts & Photo", "Technology",
|
||||
"Cookbooks", "Travel & Maps", "Poetry & Drama", "Graphic Novels"
|
||||
};
|
||||
|
||||
// 2. Curate Niche Subcategories/Tags (IsMain = false, Max 15 chars)
|
||||
var specializedSubCategories = new[]
|
||||
{
|
||||
"Cyberpunk", "Space Opera", "Historical Fix", "Cozy Mystery", "True Crime",
|
||||
"Agile Project", "Software Eng", "AI & ML", "Cloud Comput",
|
||||
"SA History", "African Lit", "Apartheid Era", "Mandela Legacy",
|
||||
"Finance", "Investments", "Startup", "Leadership",
|
||||
"CBT Therapy", "Mindfulness", "Yoga & Health",
|
||||
"Baking Basics", "African Food", "Vegan Recipes",
|
||||
"Ancient World", "WWII History", "Geopolitics",
|
||||
"Writing Guides", "Criticism", "Classic Poetry",
|
||||
"Early Learning", "Teen Romance", "Survival",
|
||||
"Urban Fantasy", "Dark Fantasy", "Psych Thriller", "Hard Sci-Fi",
|
||||
"Data Science", "DevOps", "Cybersecurity",
|
||||
"Economics", "Real Estate", "Governance",
|
||||
"Essays", "Memoirs", "Art History",
|
||||
"Architecture", "Photography", "Travel Writing",
|
||||
"Gaming Culture", "Philosophy", "Ethics",
|
||||
"DIY Home", "SA Gardening", "Parenting"
|
||||
};
|
||||
|
||||
// 3. Seed Main Categories into the System
|
||||
logger.LogInformation("Seeding broad main categories...");
|
||||
foreach (var mainCat in broadMainCategories)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) return;
|
||||
|
||||
// Defensive truncation fallback just in case strings get modified later
|
||||
string safeName = mainCat.Length > 15 ? mainCat.Substring(0, 15) : mainCat;
|
||||
|
||||
var result = await categoryService.CreateCategoryAsync(safeName, isMain: true, stoppingToken);
|
||||
if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogWarning("Notice while adding main category '{Name}': {Msg}", safeName, result.Errors[0].Message);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Seed Subcategories into the System
|
||||
logger.LogInformation("Seeding boundless specialized niche tags...");
|
||||
foreach (var subCat in specializedSubCategories)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) return;
|
||||
|
||||
string safeName = subCat.Length > 15 ? subCat.Substring(0, 15) : subCat;
|
||||
|
||||
var result = await categoryService.CreateCategoryAsync(safeName, isMain: false, stoppingToken);
|
||||
if (result.IsFailed && !result.Errors[0].Message.Contains("already exists", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogWarning("Notice while adding subcategory '{Name}': {Msg}", safeName, result.Errors[0].Message);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Query back all enabled categories to extract active IDs for junction mapping
|
||||
var fetchMainResult = await categoryService.GetCategoriesAsync(isMain: true, stoppingToken);
|
||||
var fetchSubResult = await categoryService.GetCategoriesAsync(isMain: false, stoppingToken);
|
||||
|
||||
if (fetchMainResult.IsFailed || fetchSubResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Aborting junction seeding: Could not retrieve categories from data store.");
|
||||
return;
|
||||
}
|
||||
|
||||
var mainCategoryIds = fetchMainResult.Value.Select(c => c.Id).ToArray();
|
||||
var subCategoryIds = fetchSubResult.Value.Select(c => c.Id).ToArray();
|
||||
|
||||
// 6. Map Categories to your Product Collection (Product IDs 0 - 21)
|
||||
logger.LogInformation("Beginning Product-Category mapping assignments for Product IDs 0 through 21...");
|
||||
|
||||
for (long productId = 0; productId <= 21; productId++)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
// Every book belongs to 1 or 2 main categories
|
||||
int mainCategoriesToAssign = faker.Random.Number(1, 2);
|
||||
var chosenMainIds = faker.PickRandom(mainCategoryIds, mainCategoriesToAssign).Distinct();
|
||||
|
||||
foreach (var mainId in chosenMainIds)
|
||||
{
|
||||
var linkResult = await productService.AddProductCategoryAsync(productId, mainId, stoppingToken);
|
||||
if (linkResult.IsFailed)
|
||||
{
|
||||
if (!linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogDebug("Junction note for Product {PId} and Main Category {CId}: {Msg}", productId, mainId, linkResult.Errors[0].Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every book gets 1 to 4 granular subgenre tags
|
||||
int subCategoriesToAssign = faker.Random.Number(1, 4);
|
||||
var chosenSubIds = faker.PickRandom(subCategoryIds, subCategoriesToAssign).Distinct();
|
||||
|
||||
foreach (var subId in chosenSubIds)
|
||||
{
|
||||
var linkResult = await productService.AddProductCategoryAsync(productId, subId, stoppingToken);
|
||||
if (linkResult.IsFailed && !linkResult.Errors[0].Message.Contains("exist", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogDebug("Junction note for Product {PId} and Sub Category {CId}: {Msg}", productId, subId, linkResult.Errors[0].Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("Category and Product-Tag Mapping Seeding completed successfully.");
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||
|
||||
public sealed class CdnSettings
|
||||
{
|
||||
public string? BaseCdn { get; set; }
|
||||
|
||||
public string[]? BookCovers { get; set; }
|
||||
|
||||
public string[]? Authors { get; set; }
|
||||
|
||||
public string[]? AuthorThumbnails { get; set; }
|
||||
|
||||
public string[]? BookThumbnails { get; set; }
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Customers;
|
||||
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Orders;
|
||||
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||
|
||||
public sealed class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
|
||||
ILogger<CustomerSeederService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (!await features.IsEnabledAsync("CustomerSeederService")) return;
|
||||
|
||||
logger.LogInformation("Customer Seeding started");
|
||||
|
||||
// 1: Add shipping providers (shippingProvider IDs will be created in sequence: 1, 2, 3)
|
||||
await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 39, "https://www.fastway.co.za/our-services/track-your-parcel"), stoppingToken);
|
||||
await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.DHL, "DHL Couriers", 60, "https://www.dhl.com/za-en/home/tracking.html"), stoppingToken);
|
||||
await orderService.CreateShippingProviderAsync(new CreateShippingProvider(ShippingProviderTypes.PostNet, "Postnet Overnight Mail", 45, "https://www.postnet.co.za/tracker"), stoppingToken);
|
||||
|
||||
// Initialize Bogus Faker engine
|
||||
var faker = new Faker();
|
||||
var culture = CultureInfo.InvariantCulture;
|
||||
|
||||
// Ensure repeatable datasets across executions
|
||||
Randomizer.Seed = new Random(84);
|
||||
|
||||
// South African Provinces array lookup helper
|
||||
var southAfricanProvinces = new[]
|
||||
{
|
||||
"Gauteng", "Western Cape", "KwaZulu-Natal", "Eastern Cape",
|
||||
"Free State", "Limpopo", "Mpumalanga", "North West", "Northern Cape"
|
||||
};
|
||||
|
||||
// South African major towns matching geographic boundaries roughly
|
||||
var southAfricanCities = new[] { "Midrand", "Johannesburg", "Pretoria", "Cape Town", "Durban", "Gqeberha", "Polokwane", "Nelspruit", "Bloemfontein" };
|
||||
|
||||
// Tracks sequential Address IDs added globally to the system across all loops
|
||||
long addressSequenceCounter = 0;
|
||||
|
||||
// 2: Create 15 customers with resources sequentially
|
||||
for (int c = 0; c < 15; c++)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
// Determine if this specific iteration represents a Corporate Client or an Individual Consumer
|
||||
bool isCompanyCustomer = faker.Random.Bool(0.4f); // 40% chance of seeding a corporate entity
|
||||
|
||||
string customerFirstName = faker.Name.FirstName();
|
||||
string customerLastName = faker.Name.LastName();
|
||||
|
||||
string companyName = isCompanyCustomer ? faker.Company.CompanyName() : "";
|
||||
string companySuffix = isCompanyCustomer ? faker.Company.CompanySuffix() : "";
|
||||
string fullCompanyName = isCompanyCustomer ? $"{companyName} {companySuffix}" : "";
|
||||
|
||||
string customerEmail = isCompanyCustomer
|
||||
? faker.Internet.Email(firstName: companyName, provider: "co.za").ToLower(culture)
|
||||
: faker.Internet.Email(customerFirstName, customerLastName).ToLower(culture);
|
||||
|
||||
string customerPhone = faker.Phone.PhoneNumber("087#######"); // Corporate VOIP / Personal South African cell line format
|
||||
string customerWebsite = isCompanyCustomer ? faker.Internet.Url().Replace("www.", $"www.{companyName.ToLower(culture)}.") : "";
|
||||
string customerVat = isCompanyCustomer ? faker.Phone.PhoneNumber("4#########") : ""; // SA VAT registration starts with a 4
|
||||
|
||||
// Randomly select distinct Social Media channels
|
||||
var chosenSocialType = faker.PickRandom<SocialMediaTypes>();
|
||||
string socialMediaUrl = chosenSocialType switch
|
||||
{
|
||||
SocialMediaTypes.LinkedIn => isCompanyCustomer ? $"https://linkedin.com/company/{companyName.ToLower(culture)}" : $"https://linkedin.com/in/{customerFirstName.ToLower(culture)}-{customerLastName.ToLower(culture)}",
|
||||
SocialMediaTypes.GitHub => $"https://github.com/{(isCompanyCustomer ? "orgs/" + companyName.ToLower(culture) : customerFirstName.ToLower(culture))}",
|
||||
_ => $"https://x.com/{(isCompanyCustomer ? companyName.ToLower(culture) : customerFirstName.ToLower(culture))}"
|
||||
};
|
||||
|
||||
// 3: Create customer
|
||||
var createCustomerResult = await customerService.CreateCustomerAsync(new CreateCustomer
|
||||
{
|
||||
Company = fullCompanyName,
|
||||
Email = customerEmail,
|
||||
Phone = customerPhone,
|
||||
Website = customerWebsite,
|
||||
SocialMedia =
|
||||
[
|
||||
new Models.SocialMedia
|
||||
{
|
||||
Name = chosenSocialType.ToString(),
|
||||
Type = chosenSocialType,
|
||||
ImageUrl = $"https://cdn.example.com/icons/{chosenSocialType.ToString().ToLower(culture)}.png",
|
||||
Url = socialMediaUrl
|
||||
}
|
||||
],
|
||||
VatNumber = customerVat
|
||||
}, stoppingToken);
|
||||
|
||||
if (createCustomerResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Failed to create customer record at index {Index}: {Error}", c, createCustomerResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
|
||||
var assignedCustomerId = createCustomerResult.Value;
|
||||
|
||||
// 4: Create customer contact (only if customer is a company entity)
|
||||
if (isCompanyCustomer)
|
||||
{
|
||||
var contactFirstName = faker.Name.FirstName();
|
||||
var contactLastName = faker.Name.LastName();
|
||||
|
||||
var createContactResult = await customerService.CreateCustomerContactAsync(assignedCustomerId, new CreateCustomerContact
|
||||
{
|
||||
Name = contactFirstName,
|
||||
LastName = contactLastName,
|
||||
Phone = faker.Phone.PhoneNumber("082#######"), // Typical South African mobile prefix format
|
||||
Email = faker.Internet.Email(contactFirstName, contactLastName, provider: "company.co.za").ToLower(culture),
|
||||
Type = ContactTypes.Business
|
||||
}, stoppingToken);
|
||||
|
||||
if (createContactResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Failed to create company customer contact relation: {Error}", createContactResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Shared Randomizations for Regional Postal/Building details
|
||||
var primaryState = faker.PickRandom(southAfricanProvinces);
|
||||
var primaryCity = faker.PickRandom(southAfricanCities);
|
||||
var shippingPostalCode = faker.Random.Replace("####");
|
||||
|
||||
var billingState = faker.PickRandom(southAfricanProvinces);
|
||||
var billingCity = faker.PickRandom(southAfricanCities);
|
||||
var billingPostalCode = faker.Random.Replace("####");
|
||||
|
||||
// 5: Create customer address - SHIPPING
|
||||
var createShippingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress
|
||||
{
|
||||
Name = isCompanyCustomer ? "Head Office Distribution" : "My Home Residence",
|
||||
BuildingType = faker.PickRandom<AddressBuildingTypes>(),
|
||||
Type = AddressType.Shipping,
|
||||
Street = $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street",
|
||||
City = primaryCity,
|
||||
State = primaryState,
|
||||
Country = "South Africa",
|
||||
IsPrimary = true,
|
||||
Enabled = true,
|
||||
PostalCode = shippingPostalCode
|
||||
}, stoppingToken);
|
||||
|
||||
long currentCustomerShippingAddressId = 0;
|
||||
if (createShippingAddressResult.IsSuccess)
|
||||
{
|
||||
addressSequenceCounter++;
|
||||
currentCustomerShippingAddressId = addressSequenceCounter;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogWarning("Failed to attach Shipping address profile: {Error}", createShippingAddressResult.Errors[0].Message);
|
||||
}
|
||||
|
||||
// 6: Create customer address - BILLING
|
||||
var createBillingAddressResult = await customerService.CreateCustomerAddressAsync(assignedCustomerId, new CreateCustomerAddress
|
||||
{
|
||||
Name = isCompanyCustomer ? "Accounts Payable Department" : "Billing Address",
|
||||
BuildingType = faker.PickRandom<AddressBuildingTypes>(),
|
||||
Type = AddressType.Billing,
|
||||
Street = isCompanyCustomer ? $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Boulevard" : $"{faker.Address.BuildingNumber()} {faker.Address.StreetName()} Street",
|
||||
City = billingCity,
|
||||
State = billingState,
|
||||
Country = "South Africa",
|
||||
IsPrimary = false,
|
||||
Enabled = true,
|
||||
PostalCode = billingPostalCode
|
||||
}, stoppingToken);
|
||||
|
||||
long currentCustomerBillingAddressId = 0;
|
||||
if (createBillingAddressResult.IsSuccess)
|
||||
{
|
||||
addressSequenceCounter++;
|
||||
currentCustomerBillingAddressId = addressSequenceCounter;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Failed to attach Billing address profile: {Error}", createBillingAddressResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
|
||||
// 7: Challenge Extrapolation — Create a random number of orders (0 to 4 orders) per customer
|
||||
int ordersToGenerate = faker.Random.Number(0, 4);
|
||||
for (int o = 0; o < ordersToGenerate; o++)
|
||||
{
|
||||
var deliveryInstructions = faker.PickRandom(
|
||||
"Leave at reception desk",
|
||||
"Please call before delivery",
|
||||
"At the intercom, dial 1 then option 2",
|
||||
"Leave with security guard at front gate",
|
||||
"Deliver to back delivery bay"
|
||||
);
|
||||
|
||||
// Use the calculated sequential Billing Address Id for order creation
|
||||
var orderResult = await orderService.CreateOrderAsync(
|
||||
assignedCustomerId,
|
||||
new CreateOrder(currentCustomerBillingAddressId, deliveryInstructions),
|
||||
stoppingToken
|
||||
);
|
||||
|
||||
if (orderResult.IsFailed)
|
||||
{
|
||||
logger.LogWarning("Failed to create purchase order shell context: {Error}", orderResult.Errors[0].Message);
|
||||
continue;
|
||||
}
|
||||
|
||||
long seededOrderId = orderResult.Value;
|
||||
|
||||
// Build a varying array of items using valid product bounds (IDs: 0 to 21)
|
||||
int lineItemsCount = faker.Random.Number(1, 5);
|
||||
var itemsList = new List<CreateOrderItem>();
|
||||
|
||||
for (int i = 0; i < lineItemsCount; i++)
|
||||
{
|
||||
long randomProductId = faker.Random.Number(0, 21);
|
||||
long randomProductPriceId = faker.Random.Number(0, 21);
|
||||
int itemQuantity = faker.Random.Number(1, 3);
|
||||
|
||||
itemsList.Add(new CreateOrderItem(randomProductId, randomProductPriceId, itemQuantity));
|
||||
}
|
||||
|
||||
// Push bulk items payload into order via matching test framework signatures
|
||||
var addItemsResult = await orderService.AddItemsToOrderAsync(seededOrderId, [.. itemsList], stoppingToken);
|
||||
if (addItemsResult.IsFailed)
|
||||
{
|
||||
logger.LogWarning("Failed to link item collections to Order Id {Id}", seededOrderId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Randomly select an order status matrix pathing
|
||||
var targetedOrderStatus = faker.PickRandom<OrderStatus>();
|
||||
await orderService.UpdateOrderStatusAsync(seededOrderId, targetedOrderStatus, stoppingToken);
|
||||
|
||||
// Check lifecycle workflow criteria: Attach dynamic shipping if status warrants it
|
||||
if (targetedOrderStatus != OrderStatus.Pending &&
|
||||
targetedOrderStatus != OrderStatus.Cancelled &&
|
||||
targetedOrderStatus != OrderStatus.Failed &&
|
||||
currentCustomerShippingAddressId > 0)
|
||||
{
|
||||
// Select from seeded Shipping Providers in step 1 (IDs: 1, 2, or 3)
|
||||
long randomShippingProviderId = faker.Random.Number(1, 3);
|
||||
|
||||
var addShippingResult = await orderService.AddShippingToOrderAsync(
|
||||
seededOrderId,
|
||||
new CreateShipping(currentCustomerShippingAddressId, randomShippingProviderId),
|
||||
stoppingToken
|
||||
);
|
||||
|
||||
if (addShippingResult.IsSuccess)
|
||||
{
|
||||
long assignedShippingId = addShippingResult.Value;
|
||||
|
||||
// Transition logistics flags matching delivery metrics
|
||||
var shippingStatus = faker.PickRandom<ShippingStatuses>();
|
||||
await orderService.UpdateShippingStatusAsync(seededOrderId, shippingStatus, stoppingToken);
|
||||
|
||||
if (shippingStatus == ShippingStatuses.Shipped || shippingStatus == ShippingStatuses.Delivered)
|
||||
{
|
||||
string rawTrackingCode = $"ZA{faker.Random.Replace("#########")}NV";
|
||||
await orderService.UpdateShippingTrackingNumberAsync(seededOrderId, assignedShippingId, rawTrackingCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("Successfully seeded customer profile #{Index}: {Name} alongside {Count} orders.", c, isCompanyCustomer ? fullCompanyName : $"{customerFirstName} {customerLastName}", ordersToGenerate);
|
||||
}
|
||||
|
||||
logger.LogInformation("Customer Seeding completed successfully.");
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<UserSecretsId>5c3bc894-8654-4691-99e8-f90d3414843f</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Quartz Scheduler-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.5" />
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.98">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.FeatureManagement.AspNetCore" Version="4.5.0" />
|
||||
<PackageReference Include="OpenTelemetry" Version="1.15.3" />
|
||||
<PackageReference Include="Quartz" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.Plugins" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.Plugins.TimeZoneConverter" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.Serialization.SystemTextJson" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.AspNetCore" Version="3.18.1" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.18.1" />
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="Quartz" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Configuration -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.8" />
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="Microsoft.Extensions.Configuration" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Health Checks -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Client" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Core" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.Data" Version="9.0.0" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.UI.InMemory.Storage" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="10.0.8" />
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="Microsoft.Extensions.Diagnostics.HealthChecks" />
|
||||
<Using Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Open Telemetry -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.3" />
|
||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.15.2" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.15.1" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" Version="1.15.1" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.15.3" />
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="OpenTelemetry.Resources" />
|
||||
<Using Include="OpenTelemetry.Exporter" />
|
||||
<Using Include="OpenTelemetry.Logs" />
|
||||
<Using Include="OpenTelemetry.Metrics" />
|
||||
<Using Include="OpenTelemetry.Trace" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Database -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.2" />
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="Npgsql" />
|
||||
<Using Include="Microsoft.EntityFrameworkCore" />
|
||||
<Using Include="Microsoft.EntityFrameworkCore.Design" />
|
||||
<Using Include="Microsoft.EntityFrameworkCore.Metadata.Builders" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Email -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MailKit" Version="4.17.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.17.0" />
|
||||
|
||||
<!-- Global Usings-->
|
||||
<Using Include="MimeKit" />
|
||||
<Using Include="MailKit.Net.Smtp" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- CQRS -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentResults" Version="4.0.0" />
|
||||
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
|
||||
|
||||
<!-- Global Usings -->
|
||||
<Using Include="FluentResults" />
|
||||
<Using Include="Mediator" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Amazon S3 SDK -->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.4.3" />
|
||||
<PackageReference Include="AWSSDK.S3" Version="4.0.24" />
|
||||
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
|
||||
|
||||
<!-- global Usings -->
|
||||
<Using Include="Amazon.S3" />
|
||||
<Using Include="Amazon.S3.Model" />
|
||||
<Using Include="Amazon.Runtime" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Shared Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="Bogus" />
|
||||
<Using Include="Microsoft.FeatureManagement" />
|
||||
<Using Include="System.Globalization" />
|
||||
<Using Include="System.Reflection" />
|
||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||
<Using Include="Microsoft.Extensions.Hosting" />
|
||||
<Using Include="System.Text" />
|
||||
<Using Include="System.Text.Json" />
|
||||
<Using Include="System.Threading.Channels" />
|
||||
<Using Include="System.Collections.ObjectModel" />
|
||||
<Using Include="System.Diagnostics" />
|
||||
<Using Include="System.Diagnostics.Metrics" />
|
||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<Using Include="System.Security.Cryptography" />
|
||||
<Using Include="Microsoft.Extensions.Options" />
|
||||
<Using Include="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LiteCharms.Features.MidrandBooks\LiteCharms.Features.MidrandBooks.csproj" />
|
||||
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,246 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks;
|
||||
using LiteCharms.Features.MidrandBooks.Authors;
|
||||
using LiteCharms.Features.MidrandBooks.Products;
|
||||
using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||
|
||||
public sealed class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
|
||||
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
||||
{
|
||||
private readonly CdnSettings cdnSettings = options.Value;
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (await features.IsEnabledAsync("ProductsSeederService") is not true) return;
|
||||
|
||||
logger.LogInformation("Product Seeding started");
|
||||
|
||||
if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0)
|
||||
{
|
||||
logger.LogWarning("No book covers found in CDN settings. Seeding aborted.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Bogus Faker engine
|
||||
var faker = new Faker();
|
||||
var culture = CultureInfo.InvariantCulture;
|
||||
|
||||
// Ensure repeatable data sets if run multiple times by anchoring the seed
|
||||
Randomizer.Seed = new Random(42);
|
||||
|
||||
foreach (var bookCover in cdnSettings.BookCovers)
|
||||
{
|
||||
if (stoppingToken.IsCancellationRequested) break;
|
||||
|
||||
// Generate beautifully mixed eclectic topics on the fly
|
||||
var bookTopic = faker.PickRandom(
|
||||
// --- Tech & IT ---
|
||||
"C# 12 & Modern .NET Architecture",
|
||||
"PostgreSQL Database Optimization",
|
||||
"Docker & Kubernetes in Production",
|
||||
"Domain-Driven Design Paradigms",
|
||||
"Artificial Intelligence with Python",
|
||||
|
||||
// --- Sci-Fi & Fantasy ---
|
||||
"The Chronicles of the Quantum Nebula",
|
||||
"Legends of the Lost Cybernetic Kingdom",
|
||||
"Parallel Dimensions and Rogue Time Streams",
|
||||
"The Last Android in Neo-Johannesburg",
|
||||
|
||||
// --- Thrillers, Mystery & Crime ---
|
||||
"The Midnight Code Cryptograph",
|
||||
"Shadows in the Highveld",
|
||||
"The Silent Witness of Midrand",
|
||||
"Deception on the 14th Floor",
|
||||
|
||||
// --- Business, Finance & Wealth ---
|
||||
"Mastering the South African Tech Market",
|
||||
"The Modern Entrepreneur's Blueprint",
|
||||
"Generational Wealth and Venture Capital",
|
||||
"Negotiation Tactics for High-Stakes Deals",
|
||||
|
||||
// --- Self-Help & Personal Growth ---
|
||||
"The Art of Relentless Focus",
|
||||
"Building High-Performance Habits",
|
||||
"The Mindfulness Guide for Software Engineers",
|
||||
"Unlocking Creative Flow Under Pressure"
|
||||
);
|
||||
|
||||
// Dynamic raw title generation formulas executed via random function picker
|
||||
var titlePatterns = new Func<string>[]
|
||||
{
|
||||
() => $"{faker.Company.CatchPhrase()} with {bookTopic}",
|
||||
() => $"The {faker.Commerce.ProductAdjective()} Guide to {bookTopic}",
|
||||
() => $"Mastering {bookTopic}: A {faker.Company.Bs()} Blueprint",
|
||||
() => $"{bookTopic} for the Modern {faker.Name.JobTitle()}",
|
||||
() => $"Advanced {bookTopic}: Demystifying the {faker.Company.CatchPhrase()}",
|
||||
() => $"{faker.Random.Replace("###")} Blueprints for {bookTopic}"
|
||||
};
|
||||
|
||||
// Pick a format template and resolve it down to raw string text
|
||||
var rawTitle = faker.PickRandom(titlePatterns)();
|
||||
var bookTitle = rawTitle.Length > 255 ? rawTitle[..252] + "..." : rawTitle;
|
||||
|
||||
var rawSummary = $"A comprehensive guide to mastering {bookTopic}. Learn modern implementation techniques through real-world software engineering paradigms.";
|
||||
var bookSummary = rawSummary.Length > 512 ? rawSummary[..509] + "..." : rawSummary;
|
||||
|
||||
// Generating a single concise paragraph ensures a rich text description falling safely well under 1024
|
||||
var rawDescription = faker.Lorem.Paragraph(3);
|
||||
var bookDescription = rawDescription.Length > 1024 ? rawDescription[..1021] + "..." : rawDescription;
|
||||
|
||||
var authorFirstName = faker.Name.FirstName();
|
||||
var authorLastName = faker.Name.LastName();
|
||||
var publisherCompany = faker.Company.CompanyName();
|
||||
|
||||
// Safe bounded random picking for book thumbnails
|
||||
string? pickedBookThumbnail = null;
|
||||
string? pickedBookThumbnail1 = null;
|
||||
string? pickedBookThumbnail2 = null;
|
||||
string? pickedBookThumbnail3 = null;
|
||||
string? pickedBookThumbnail4 = null;
|
||||
if (cdnSettings.BookThumbnails is not null && cdnSettings.BookThumbnails.Length > 0)
|
||||
{
|
||||
pickedBookThumbnail = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
||||
pickedBookThumbnail1 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
||||
pickedBookThumbnail2 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
||||
pickedBookThumbnail3 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
||||
pickedBookThumbnail4 = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.BookThumbnails)}";
|
||||
}
|
||||
|
||||
// Step 1: Add Product
|
||||
var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct
|
||||
{
|
||||
Name = bookTitle,
|
||||
Summary = bookSummary,
|
||||
Description = bookDescription,
|
||||
ImageUrl = $"{cdnSettings.BaseCdn}{bookCover}",
|
||||
Type = ProductTypes.Book,
|
||||
Metadata = new Models.ProductMetadata
|
||||
{
|
||||
CopyrightInfo = $"© {DateTime.UtcNow.Year} {publisherCompany}. All rights reserved.",
|
||||
ManufactureDate = faker.Date.Past(3).ToString("yyyy-MM-dd", culture),
|
||||
Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}",
|
||||
SerialNumber = faker.Phone.PhoneNumber("978-##########")
|
||||
},
|
||||
Categories = ["Coding", "Computers", "IT"],
|
||||
ThumbnailUrls = pickedBookThumbnail is not null ? [pickedBookThumbnail, pickedBookThumbnail1!, pickedBookThumbnail2!, pickedBookThumbnail3!, pickedBookThumbnail4!] : null
|
||||
}, stoppingToken);
|
||||
|
||||
if (productCreateResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Failed to create product: {Error}", productCreateResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
|
||||
// Step 2: Enable product so it can show on the shop
|
||||
var enableProductResult = await productService.UpdateProductStatusAsync(productId: productCreateResult.Value, isEnabled: true, stoppingToken);
|
||||
|
||||
if (enableProductResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Failed to enable created product: {Error}", enableProductResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
|
||||
// Step 3: Create Product Price
|
||||
var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice
|
||||
{
|
||||
Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2),
|
||||
Discount = 0.0m
|
||||
}, stoppingToken);
|
||||
|
||||
if (productPriceCreateResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Failed to create product price: {Error}", productPriceCreateResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
|
||||
// Safe bounded picking for Authors (Real Avatars)
|
||||
string authorAvatarUrl = faker.Internet.Avatar(); // Fallback
|
||||
if (cdnSettings.Authors is not null && cdnSettings.Authors.Length > 0)
|
||||
{
|
||||
authorAvatarUrl = $"{cdnSettings.BaseCdn}{faker.PickRandom(cdnSettings.Authors)}";
|
||||
}
|
||||
|
||||
// Safe bounded picking for Author Thumbnails (Cartoon Avatars)
|
||||
string? authorThumbnailUrl = null;
|
||||
if (cdnSettings.AuthorThumbnails is not null && cdnSettings.AuthorThumbnails.Length > 0)
|
||||
{
|
||||
var selectedThumb = faker.PickRandom(cdnSettings.AuthorThumbnails);
|
||||
authorThumbnailUrl = $"{cdnSettings.BaseCdn}{selectedThumb}.jpg";
|
||||
}
|
||||
|
||||
// Synthesize a highly dynamic, organic opening bio statement
|
||||
var professionalBackgrounds = new[]
|
||||
{
|
||||
$"{authorFirstName} {authorLastName} is an award-winning {faker.Name.JobDescriptor()} {faker.Name.JobTitle()} with over {faker.Random.Number(5, 25)} years of core engineering domain expertise.",
|
||||
$"As a veteran systems consultant and practicing {faker.Name.JobTitle()}, {authorFirstName} has spent decades leading digital infrastructure transformations and managing complex topologies.",
|
||||
$"Operating from modern innovation hubs, {authorFirstName} {authorLastName} specializes in global product strategies and serves as an authority in {faker.Name.JobDescriptor()} computing.",
|
||||
$"With a rich professional background as a principal {faker.Name.JobTitle()} at {publisherCompany}, {authorFirstName} has spent a lifetime refining the system workflows highlighted here."
|
||||
};
|
||||
|
||||
// Pick a randomized context hook and append a 2-paragraph contextual narrative block
|
||||
var biographyPrefix = faker.PickRandom(professionalBackgrounds);
|
||||
var authorBiography = $"{biographyPrefix} {faker.Lorem.Paragraph(2)}";
|
||||
|
||||
// Step 4: Create Author
|
||||
var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor
|
||||
{
|
||||
Name = authorFirstName,
|
||||
LastName = authorLastName,
|
||||
Company = publisherCompany,
|
||||
VatNumber = faker.Random.Bool() ? faker.Phone.PhoneNumber("4#########") : "",
|
||||
PublisherType = faker.PickRandom<PublisherTypes>(),
|
||||
Email = faker.Internet.Email(authorFirstName, authorLastName),
|
||||
Website = faker.Internet.Url(),
|
||||
ImageUrl = authorAvatarUrl,
|
||||
SocialMedia =
|
||||
[
|
||||
new Models.SocialMedia
|
||||
{
|
||||
Name = "LinkedIn",
|
||||
ImageUrl = "https://cdn.example.com/icons/linkedin.png",
|
||||
Type = SocialMediaTypes.LinkedIn,
|
||||
Url = $"https://linkedin.com/in/{authorFirstName.ToLower(culture)}-{authorLastName.ToLower(culture)}"
|
||||
},
|
||||
new Models.SocialMedia
|
||||
{
|
||||
Name = "GitHub",
|
||||
ImageUrl = "https://cdn.example.com/icons/github.png",
|
||||
Type = SocialMediaTypes.GitHub,
|
||||
Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}"
|
||||
}
|
||||
],
|
||||
Biography = authorBiography,
|
||||
ThumbnailImageUrl = authorThumbnailUrl
|
||||
}, stoppingToken);
|
||||
|
||||
if (authorCreateResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Failed to create author: {Error}", authorCreateResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
|
||||
// Step 5: Create Author-Book link (product linkage)
|
||||
var authorBookCreateResult = await booksService.CreateBookAsync(authorId: authorCreateResult.Value, productId: productCreateResult.Value, stoppingToken);
|
||||
|
||||
if (authorBookCreateResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Failed to create author-book linkage: {Error}", authorBookCreateResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
|
||||
var enableAuthorBookResult = await booksService.UpdateBookStatusAsync(bookId: authorBookCreateResult.Value, isEnabled: true, stoppingToken);
|
||||
|
||||
if (enableAuthorBookResult.IsFailed)
|
||||
{
|
||||
logger.LogError("Failed to enable author-book link: {Error}", enableAuthorBookResult.Errors[0].Message);
|
||||
break;
|
||||
}
|
||||
|
||||
logger.LogInformation("Successfully seeded book product: {Title}", bookTitle);
|
||||
}
|
||||
|
||||
logger.LogInformation("Product Seeding completed successfully.");
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Seed;
|
||||
using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
builder.Configuration
|
||||
.AddCommandLine(args)
|
||||
.AddUserSecrets(typeof(Program).Assembly)
|
||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables();
|
||||
|
||||
builder.Services.AddScopedFeatureManagement();
|
||||
|
||||
builder.Services
|
||||
.AddLogging()
|
||||
.AddShopServices()
|
||||
.AddHostedService<ProductsSeederService>()
|
||||
.AddHostedService<CategorySeederService>()
|
||||
.AddHostedService<CustomerSeederService>()
|
||||
.AddMidrandShopDatabase(builder.Configuration);
|
||||
|
||||
builder.Services.Configure<CdnSettings>(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options));
|
||||
|
||||
using var host = builder.Build();
|
||||
|
||||
await host.RunAsync();
|
||||
@@ -1,266 +0,0 @@
|
||||
{
|
||||
"FeatureManagement": {
|
||||
"CategorySeederService": true,
|
||||
"CustomerSeederService": false,
|
||||
"ProductsSeederService": false
|
||||
},
|
||||
"CdnSettings": {
|
||||
"BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/",
|
||||
"BookCovers": [
|
||||
"144e0314-0bd8-4e2c-8814-2b34608c0600_1764780116467.webp",
|
||||
"2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp",
|
||||
"3cd172b9-416e-4b3a-b613-7ff0a3aecc4c_1764780129459.webp",
|
||||
"762947b6-63ac-436e-98fb-91dd94de1d82_1764780126141.webp",
|
||||
"7b42f23f-666c-4dd9-82e1-9b928e1b4db7_1764780186376.webp",
|
||||
"8e281eea-8910-473d-b650-43f539995d9e_1764780152316.webp",
|
||||
"91d18e2c-6ee6-44b4-84a5-8692db5dbfe4_1764780114484.webp",
|
||||
"94fdf403-4d10-4cb4-a537-fc914a1f5ba8_1764780198423.webp",
|
||||
"9b881786-75e1-47f7-9bc9-27188d46d1ec_1764780122458.webp",
|
||||
"c417236d-a628-45eb-82c2-ec1cb0326e4f_1765006317965.webp",
|
||||
"clpqjsgi71jpr1i6392e449l2.webp",
|
||||
"clps1mk9f0m1z1i32grvq17tg.webp",
|
||||
"clq550xxy2dab1ywb6mdv4acv.webp",
|
||||
"clq5535o52dj51yw557ml49c1.webp",
|
||||
"clq55455w2dcm1yvd8d8258d8.webp",
|
||||
"clq558fkh2djy1ywbghwcawnh.webp",
|
||||
"clq55cvqh2dqi1yyratl123wq.webp",
|
||||
"clrhnk94e115k1y00bg8h9ixb.webp",
|
||||
"d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp",
|
||||
"e6ba52f208914285bcdf1966cfb08f6f.jpg",
|
||||
"fa9cbbe6-f947-4f83-8e98-61d2661f43e0_1764841636705.webp"
|
||||
],
|
||||
"Authors": [
|
||||
"authors/uifaces-human-avatar.jpg",
|
||||
"authors/uifaces-human-avatar-1.jpg",
|
||||
"authors/uifaces-human-avatar-2.jpg",
|
||||
"authors/uifaces-human-avatar-3.jpg",
|
||||
"authors/uifaces-human-avatar-4.jpg",
|
||||
"authors/uifaces-human-avatar-5.jpg",
|
||||
"authors/uifaces-human-avatar-6.jpg",
|
||||
"authors/uifaces-human-avatar-7.jpg",
|
||||
"authors/uifaces-human-avatar-8.jpg",
|
||||
"authors/uifaces-human-avatar-9.jpg",
|
||||
"authors/uifaces-human-avatar-10.jpg",
|
||||
"authors/uifaces-human-avatar-11.jpg",
|
||||
"authors/uifaces-human-avatar-12.jpg",
|
||||
"authors/uifaces-human-avatar-13.jpg",
|
||||
"authors/uifaces-human-avatar-14.jpg",
|
||||
"authors/uifaces-human-avatar-15.jpg",
|
||||
"authors/uifaces-human-avatar-16.jpg"
|
||||
],
|
||||
"AuthorThumbnails": [
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-1",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-2",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-3",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-4",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-5",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-6",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-7",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-8",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-9",
|
||||
"authors/thumbnails/uifaces-cartoon-avatar-10"
|
||||
],
|
||||
"BookThumbnails": [
|
||||
"thumbnails/book_thumbnail_001.jpg",
|
||||
"thumbnails/book_thumbnail_002.jpg",
|
||||
"thumbnails/book_thumbnail_003.jpg",
|
||||
"thumbnails/book_thumbnail_004.jpg",
|
||||
"thumbnails/book_thumbnail_005.jpg",
|
||||
"thumbnails/book_thumbnail_006.jpg",
|
||||
"thumbnails/book_thumbnail_007.jpg",
|
||||
"thumbnails/book_thumbnail_008.jpg",
|
||||
"thumbnails/book_thumbnail_009.jpg",
|
||||
"thumbnails/book_thumbnail_010.jpg",
|
||||
"thumbnails/book_thumbnail_011.jpg",
|
||||
"thumbnails/book_thumbnail_012.jpg",
|
||||
"thumbnails/book_thumbnail_013.jpg",
|
||||
"thumbnails/book_thumbnail_014.jpg",
|
||||
"thumbnails/book_thumbnail_015.jpg",
|
||||
"thumbnails/book_thumbnail_016.jpg",
|
||||
"thumbnails/book_thumbnail_017.jpg",
|
||||
"thumbnails/book_thumbnail_018.jpg",
|
||||
"thumbnails/book_thumbnail_019.jpg",
|
||||
"thumbnails/book_thumbnail_020.jpg",
|
||||
"thumbnails/book_thumbnail_021.jpg",
|
||||
"thumbnails/book_thumbnail_022.jpg",
|
||||
"thumbnails/book_thumbnail_023.jpg",
|
||||
"thumbnails/book_thumbnail_024.jpg",
|
||||
"thumbnails/book_thumbnail_025.jpg",
|
||||
"thumbnails/book_thumbnail_026.jpg",
|
||||
"thumbnails/book_thumbnail_027.jpg",
|
||||
"thumbnails/book_thumbnail_028.jpg",
|
||||
"thumbnails/book_thumbnail_029.jpg",
|
||||
"thumbnails/book_thumbnail_030.jpg",
|
||||
"thumbnails/book_thumbnail_031.jpg",
|
||||
"thumbnails/book_thumbnail_032.jpg",
|
||||
"thumbnails/book_thumbnail_033.jpg",
|
||||
"thumbnails/book_thumbnail_034.jpg",
|
||||
"thumbnails/book_thumbnail_035.jpg",
|
||||
"thumbnails/book_thumbnail_036.jpg",
|
||||
"thumbnails/book_thumbnail_037.jpg",
|
||||
"thumbnails/book_thumbnail_038.jpg",
|
||||
"thumbnails/book_thumbnail_039.jpg",
|
||||
"thumbnails/book_thumbnail_040.jpg",
|
||||
"thumbnails/book_thumbnail_041.jpg",
|
||||
"thumbnails/book_thumbnail_042.jpg",
|
||||
"thumbnails/book_thumbnail_043.jpg",
|
||||
"thumbnails/book_thumbnail_044.jpg",
|
||||
"thumbnails/book_thumbnail_045.jpg",
|
||||
"thumbnails/book_thumbnail_046.jpg",
|
||||
"thumbnails/book_thumbnail_047.jpg",
|
||||
"thumbnails/book_thumbnail_048.jpg",
|
||||
"thumbnails/book_thumbnail_049.jpg",
|
||||
"thumbnails/book_thumbnail_050.jpg",
|
||||
"thumbnails/book_thumbnail_051.jpg",
|
||||
"thumbnails/book_thumbnail_052.jpg",
|
||||
"thumbnails/book_thumbnail_053.jpg",
|
||||
"thumbnails/book_thumbnail_054.jpg",
|
||||
"thumbnails/book_thumbnail_055.jpg",
|
||||
"thumbnails/book_thumbnail_056.jpg",
|
||||
"thumbnails/book_thumbnail_057.jpg",
|
||||
"thumbnails/book_thumbnail_058.jpg",
|
||||
"thumbnails/book_thumbnail_059.jpg",
|
||||
"thumbnails/book_thumbnail_060.jpg",
|
||||
"thumbnails/book_thumbnail_061.jpg",
|
||||
"thumbnails/book_thumbnail_062.jpg",
|
||||
"thumbnails/book_thumbnail_063.jpg",
|
||||
"thumbnails/book_thumbnail_064.jpg",
|
||||
"thumbnails/book_thumbnail_065.jpg",
|
||||
"thumbnails/book_thumbnail_066.jpg",
|
||||
"thumbnails/book_thumbnail_067.jpg",
|
||||
"thumbnails/book_thumbnail_068.jpg",
|
||||
"thumbnails/book_thumbnail_069.jpg",
|
||||
"thumbnails/book_thumbnail_070.jpg",
|
||||
"thumbnails/book_thumbnail_071.jpg",
|
||||
"thumbnails/book_thumbnail_072.jpg",
|
||||
"thumbnails/book_thumbnail_073.jpg",
|
||||
"thumbnails/book_thumbnail_074.jpg",
|
||||
"thumbnails/book_thumbnail_075.jpg",
|
||||
"thumbnails/book_thumbnail_076.jpg",
|
||||
"thumbnails/book_thumbnail_077.jpg",
|
||||
"thumbnails/book_thumbnail_078.jpg",
|
||||
"thumbnails/book_thumbnail_079.jpg",
|
||||
"thumbnails/book_thumbnail_080.jpg",
|
||||
"thumbnails/book_thumbnail_081.jpg",
|
||||
"thumbnails/book_thumbnail_082.jpg",
|
||||
"thumbnails/book_thumbnail_083.jpg",
|
||||
"thumbnails/book_thumbnail_084.jpg",
|
||||
"thumbnails/book_thumbnail_085.jpg",
|
||||
"thumbnails/book_thumbnail_086.jpg",
|
||||
"thumbnails/book_thumbnail_087.jpg",
|
||||
"thumbnails/book_thumbnail_088.jpg",
|
||||
"thumbnails/book_thumbnail_089.jpg",
|
||||
"thumbnails/book_thumbnail_090.jpg",
|
||||
"thumbnails/book_thumbnail_091.jpg",
|
||||
"thumbnails/book_thumbnail_092.jpg",
|
||||
"thumbnails/book_thumbnail_093.jpg",
|
||||
"thumbnails/book_thumbnail_094.jpg",
|
||||
"thumbnails/book_thumbnail_095.jpg",
|
||||
"thumbnails/book_thumbnail_096.jpg",
|
||||
"thumbnails/book_thumbnail_097.jpg",
|
||||
"thumbnails/book_thumbnail_098.jpg",
|
||||
"thumbnails/book_thumbnail_099.jpg",
|
||||
"thumbnails/book_thumbnail_100.jpg",
|
||||
"thumbnails/book_thumbnail_101.jpg",
|
||||
"thumbnails/book_thumbnail_102.jpg",
|
||||
"thumbnails/book_thumbnail_103.jpg",
|
||||
"thumbnails/book_thumbnail_104.jpg",
|
||||
"thumbnails/book_thumbnail_105.jpg",
|
||||
"thumbnails/book_thumbnail_106.jpg",
|
||||
"thumbnails/book_thumbnail_107.jpg",
|
||||
"thumbnails/book_thumbnail_108.jpg",
|
||||
"thumbnails/book_thumbnail_109.jpg",
|
||||
"thumbnails/book_thumbnail_110.jpg",
|
||||
"thumbnails/book_thumbnail_111.jpg",
|
||||
"thumbnails/book_thumbnail_112.jpg",
|
||||
"thumbnails/book_thumbnail_113.jpg",
|
||||
"thumbnails/book_thumbnail_114.jpg",
|
||||
"thumbnails/book_thumbnail_115.jpg",
|
||||
"thumbnails/book_thumbnail_116.jpg",
|
||||
"thumbnails/book_thumbnail_117.jpg",
|
||||
"thumbnails/book_thumbnail_118.jpg",
|
||||
"thumbnails/book_thumbnail_119.jpg",
|
||||
"thumbnails/book_thumbnail_120.jpg",
|
||||
"thumbnails/book_thumbnail_121.jpg",
|
||||
"thumbnails/book_thumbnail_122.jpg",
|
||||
"thumbnails/book_thumbnail_123.jpg",
|
||||
"thumbnails/book_thumbnail_124.jpg",
|
||||
"thumbnails/book_thumbnail_125.jpg",
|
||||
"thumbnails/book_thumbnail_126.jpg",
|
||||
"thumbnails/book_thumbnail_127.jpg",
|
||||
"thumbnails/book_thumbnail_128.jpg",
|
||||
"thumbnails/book_thumbnail_129.jpg",
|
||||
"thumbnails/book_thumbnail_130.jpg",
|
||||
"thumbnails/book_thumbnail_131.jpg",
|
||||
"thumbnails/book_thumbnail_132.jpg",
|
||||
"thumbnails/book_thumbnail_133.jpg",
|
||||
"thumbnails/book_thumbnail_134.jpg",
|
||||
"thumbnails/book_thumbnail_135.jpg",
|
||||
"thumbnails/book_thumbnail_136.jpg",
|
||||
"thumbnails/book_thumbnail_137.jpg",
|
||||
"thumbnails/book_thumbnail_138.jpg",
|
||||
"thumbnails/book_thumbnail_139.jpg",
|
||||
"thumbnails/book_thumbnail_140.jpg",
|
||||
"thumbnails/book_thumbnail_141.jpg",
|
||||
"thumbnails/book_thumbnail_142.jpg",
|
||||
"thumbnails/book_thumbnail_143.jpg",
|
||||
"thumbnails/book_thumbnail_144.jpg",
|
||||
"thumbnails/book_thumbnail_145.jpg",
|
||||
"thumbnails/book_thumbnail_146.jpg",
|
||||
"thumbnails/book_thumbnail_147.jpg",
|
||||
"thumbnails/book_thumbnail_148.jpg",
|
||||
"thumbnails/book_thumbnail_149.jpg",
|
||||
"thumbnails/book_thumbnail_150.jpg",
|
||||
"thumbnails/book_thumbnail_151.jpg",
|
||||
"thumbnails/book_thumbnail_152.jpg",
|
||||
"thumbnails/book_thumbnail_153.jpg",
|
||||
"thumbnails/book_thumbnail_154.jpg",
|
||||
"thumbnails/book_thumbnail_155.jpg",
|
||||
"thumbnails/book_thumbnail_156.jpg",
|
||||
"thumbnails/book_thumbnail_157.jpg",
|
||||
"thumbnails/book_thumbnail_158.jpg",
|
||||
"thumbnails/book_thumbnail_159.jpg",
|
||||
"thumbnails/book_thumbnail_160.jpg",
|
||||
"thumbnails/book_thumbnail_161.jpg",
|
||||
"thumbnails/book_thumbnail_162.jpg",
|
||||
"thumbnails/book_thumbnail_163.jpg",
|
||||
"thumbnails/book_thumbnail_164.jpg",
|
||||
"thumbnails/book_thumbnail_165.jpg",
|
||||
"thumbnails/book_thumbnail_166.jpg",
|
||||
"thumbnails/book_thumbnail_167.jpg",
|
||||
"thumbnails/book_thumbnail_168.jpg",
|
||||
"thumbnails/book_thumbnail_169.jpg",
|
||||
"thumbnails/book_thumbnail_170.jpg",
|
||||
"thumbnails/book_thumbnail_171.jpg",
|
||||
"thumbnails/book_thumbnail_172.jpg",
|
||||
"thumbnails/book_thumbnail_173.jpg",
|
||||
"thumbnails/book_thumbnail_174.jpg",
|
||||
"thumbnails/book_thumbnail_175.jpg",
|
||||
"thumbnails/book_thumbnail_176.jpg",
|
||||
"thumbnails/book_thumbnail_177.jpg",
|
||||
"thumbnails/book_thumbnail_178.jpg",
|
||||
"thumbnails/book_thumbnail_179.jpg",
|
||||
"thumbnails/book_thumbnail_180.jpg",
|
||||
"thumbnails/book_thumbnail_181.jpg",
|
||||
"thumbnails/book_thumbnail_182.jpg",
|
||||
"thumbnails/book_thumbnail_183.jpg",
|
||||
"thumbnails/book_thumbnail_184.jpg",
|
||||
"thumbnails/book_thumbnail_185.jpg",
|
||||
"thumbnails/book_thumbnail_186.jpg",
|
||||
"thumbnails/book_thumbnail_187.jpg",
|
||||
"thumbnails/book_thumbnail_188.jpg",
|
||||
"thumbnails/book_thumbnail_189.jpg",
|
||||
"thumbnails/book_thumbnail_190.jpg",
|
||||
"thumbnails/book_thumbnail_191.jpg",
|
||||
"thumbnails/book_thumbnail_192.jpg",
|
||||
"thumbnails/book_thumbnail_193.jpg",
|
||||
"thumbnails/book_thumbnail_194.jpg",
|
||||
"thumbnails/book_thumbnail_195.jpg",
|
||||
"thumbnails/book_thumbnail_196.jpg",
|
||||
"thumbnails/book_thumbnail_197.jpg",
|
||||
"thumbnails/book_thumbnail_198.jpg",
|
||||
"thumbnails/book_thumbnail_199.jpg",
|
||||
"thumbnails/book_thumbnail_200.jpg"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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) : 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);
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
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,70 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Categories;
|
||||
using LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Tests;
|
||||
|
||||
public class CategoryServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||
{
|
||||
private readonly CategoryService categoryService = fixture.Services.GetRequiredService<CategoryService>();
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task UpdateCategoryStatusAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var result = await categoryService.UpdateCategoryStatusAsync(3, false, false, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task GetCategoryAsync_ShouldReturn_ResultWithCategory()
|
||||
{
|
||||
var result = await categoryService.GetCategoryAsync(3, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotNull(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task GetCategoriesAsync_ShouldReturn_All_ResultWithCategoryList()
|
||||
{
|
||||
var result = await categoryService.GetCategoriesAsync(isMain: null,fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotEmpty(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task GetCategoriesAsync_ShouldReturn_MainCategory_ResultWithCategoryList()
|
||||
{
|
||||
var result = await categoryService.GetCategoriesAsync(true, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotEmpty(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task GetCategoriesAsync_ShouldReturn_SubMainCategory_ResultWithCategoryList()
|
||||
{
|
||||
var result = await categoryService.GetCategoriesAsync(false, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotEmpty(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task CreateCategoriesAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var result = await categoryService.CreateCategoriesAsync(fixture.CancellationToken, "Test", "Test 1", "Test 2");
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task CreateCategoryAsync_ShouldReturn_ResultWithCategoryId()
|
||||
{
|
||||
var result = await categoryService.CreateCategoryAsync("Test", true, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using LiteCharms.Features.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||
|
||||
public class Fixture : IDisposable
|
||||
{
|
||||
public IConfiguration Configuration { get; set; }
|
||||
|
||||
public IServiceProvider Services { get; set; }
|
||||
|
||||
public IMediator Mediator { get; set; }
|
||||
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||
|
||||
public CancellationToken CancellationToken => cancellationTokenSource.Token;
|
||||
|
||||
public Fixture()
|
||||
{
|
||||
Configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddUserSecrets<Fixture>()
|
||||
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
Services = new ServiceCollection()
|
||||
.AddHttpClient()
|
||||
.AddMediator()
|
||||
.AddLogging()
|
||||
.AddEmailServiceBus()
|
||||
.AddGarageS3(Configuration)
|
||||
.AddMidrandShopDatabase(Configuration)
|
||||
.AddEmailServices(Configuration)
|
||||
.AddSingleton(Configuration)
|
||||
.AddShopServices()
|
||||
.AddHashServices(Configuration)
|
||||
.BuildServiceProvider();
|
||||
|
||||
Mediator = Services.GetRequiredService<IMediator>();
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||
|
||||
public class IntegrationFactAttribute : FactAttribute
|
||||
{
|
||||
public IntegrationFactAttribute()
|
||||
{
|
||||
if(!Debugger.IsAttached)
|
||||
Skip = "This test requires the debugger to be attached.";
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<UserSecretsId>b205af96-ceef-44e1-851c-458c9fd1c437</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Global Usings -->
|
||||
<ItemGroup>
|
||||
<Using Include="Mediator" />
|
||||
<Using Include="Xunit.Abstractions" />
|
||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||
<Using Include="Microsoft.Extensions.Configuration" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LiteCharms.Features.MidrandBooks\LiteCharms.Features.MidrandBooks.csproj" />
|
||||
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="System.Net" />
|
||||
<Using Include="System.Text.Json" />
|
||||
<Using Include="System.Diagnostics" />
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,196 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Payments;
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Tests;
|
||||
|
||||
public sealed class PayfastServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||
{
|
||||
private readonly PayfastService payfastService = fixture.Services.GetRequiredService<PayfastService>();
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithGatewayLedgerId()
|
||||
{
|
||||
var request = new CreateGatewayLedgerEntry
|
||||
{
|
||||
OrderId = 1,
|
||||
PaymentId = 1,
|
||||
MerchantPaymentId = "M_REF_TEST_99",
|
||||
PayfastPaymentId = "PF_SYS_ID_10023",
|
||||
CustomerEmail = "buyer@litecharms.co.za",
|
||||
AmountGross = 350.00m,
|
||||
AmountFee = 12.50m,
|
||||
AmountNet = 337.50m,
|
||||
PaymentStatus = "COMPLETE"
|
||||
};
|
||||
|
||||
var result = await payfastService.WriteLedgerEntryAsync(request, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task ValidateReferrerIpAsync_WithValidPayfastHostIp_ShouldReturnTrue()
|
||||
{
|
||||
var addresses = await Dns.GetHostAddressesAsync("sandbox.payfast.co.za", fixture.CancellationToken);
|
||||
|
||||
string liveTargetIp = addresses.First().ToString();
|
||||
|
||||
var result = await payfastService.ValidateReferrerIpAsync(liveTargetIp, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task ValidateReferrerIpAsync_WithUntrustedIp_ShouldReturnFalse()
|
||||
{
|
||||
string rogueIp = "8.8.8.8";
|
||||
|
||||
var result = await payfastService.ValidateReferrerIpAsync(rogueIp, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public void ValidatePaymentAmount_WhenWithinAllowableDelta_ShouldReturnTrue()
|
||||
{
|
||||
decimal systemExpectedTotal = 199.99m;
|
||||
string gatewayClearedGross = "200.00"; // Variance is exactly R0.01
|
||||
|
||||
var result = payfastService.ValidatePaymentAmount(systemExpectedTotal, gatewayClearedGross);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public void ValidatePaymentAmount_WhenVarianceBreachesDeltaBounds_ShouldReturnFalse()
|
||||
{
|
||||
decimal systemExpectedTotal = 199.99m;
|
||||
string gatewayClearedGross = "150.00";
|
||||
|
||||
var result = payfastService.ValidatePaymentAmount(systemExpectedTotal, gatewayClearedGross);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task ValidateServerConfirmationAsync_WithUnrecognizedPayload_ShouldReturnFalseFromCentralGateway()
|
||||
{
|
||||
// Arrange - Execute against actual Payfast servers using raw mock parameters.
|
||||
// The server handshake will return 200 OK with string payload 'INVALID'
|
||||
string arbitraryParameters = "merchant_id=10000000&payment_status=COMPLETE";
|
||||
|
||||
var result = await payfastService.ValidateServerConfirmationAsync(arbitraryParameters, isSandbox: true, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(result.Value); // Handshake data rejected as fraudulent/unrecognized
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public void GenerateSignature_WithStandardTelemetryData_ShouldSucceedAndHashString()
|
||||
{
|
||||
var telemetryPayload = new Dictionary<string, string?>
|
||||
{
|
||||
{ "merchant_id", "10049307" },
|
||||
{ "merchant_key", "ju6navn0jcbf0" },
|
||||
{ "amount_gross", "250.00" },
|
||||
{ "item_name", "Midrand School Textbook Variant A" }
|
||||
};
|
||||
|
||||
string passphrase = "oauth_test_signature_pass";
|
||||
|
||||
var result = PayfastService.GenerateSignature(telemetryPayload, passphrase);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(string.IsNullOrWhiteSpace(result.Value));
|
||||
Assert.Equal(32, result.Value.Length); // MD5 outputs hex representations totaling 32 characters
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Payments;
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Tests;
|
||||
|
||||
public sealed class PaymentServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||
{
|
||||
private readonly PaymentService paymentService = fixture.Services.GetRequiredService<PaymentService>();
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task CreateRefundAsync_ShouldReturn_ResultWithRefundId()
|
||||
{
|
||||
var request = new CreateRefund
|
||||
{
|
||||
Amount = 50,
|
||||
OrderId = 2,
|
||||
Type = RefundTypes.Partial,
|
||||
Reason = "Returned damaged book",
|
||||
Status = RefundStatus.Completed,
|
||||
};
|
||||
|
||||
var result = await paymentService.CreateRefundAsync(request, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var request = new CreateLedgerEntry
|
||||
{
|
||||
CustomerId = 1,
|
||||
OrderId = 1,
|
||||
PaymentGatewayId = 1,
|
||||
PaymentGatewayReference = "TEST REFERENCE",
|
||||
PaymentId = 1,
|
||||
Status = LedgerStatuses.Received,
|
||||
};
|
||||
|
||||
var result = await paymentService.WriteLedgerEntryAsync(request, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task GetPaymentGatewayAsync_ShouldReturn_ResultWithPaymentGateway()
|
||||
{
|
||||
var result = await paymentService.GetPaymentGatewayAsync(1, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotNull(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task CreatePaymentGatewayAsync_ShouldReturn_ResultWithGatewayId()
|
||||
{
|
||||
var request = new CreatePaymentGateway
|
||||
{
|
||||
IsSandbox = true,
|
||||
MerchantId = "10049307",
|
||||
MerchantKey = "ju6navn0jcbf0",
|
||||
Name = "Payfast",
|
||||
Website = "https://sandbox.payfast.co.za/eng/process",
|
||||
};
|
||||
|
||||
var result = await paymentService.CreatePaymentGatewayAsync(request, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task CompletePaymentAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var result = await paymentService.CompletePaymentAsync(1, PaymentStatuses.Paid, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task UpdatePaymentAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var result = await paymentService.UpdatePaymentAsync(1, 200, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task CreatePaymentAsync_ShouldReturn_ResultWithPaymentId()
|
||||
{
|
||||
var result = await paymentService.CreatePaymentAsync(100, 1, "HASHEDID", fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
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 CheckProductStockAvailabilityAsync_ShouldReturn_ResultWithProductInventory()
|
||||
{
|
||||
var result = await productService.CheckProductStockAvailabilityAsync(1, 1, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotNull(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task ReserveProductInventoryAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var request = new ReserveStock
|
||||
{
|
||||
ProductId = 1,
|
||||
ProductPriceId = 1,
|
||||
Reservation = 100,
|
||||
};
|
||||
|
||||
var result = await productService.ReserveProductInventoryAsync(request, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task AllocateProductInventoryAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var request = new AllocateStock
|
||||
{
|
||||
ProductId = 1,
|
||||
ProductPriceId = 1,
|
||||
Allocation = 500,
|
||||
};
|
||||
|
||||
var result = await productService.AllocateProductInventoryAsync(request, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.True(result.Value > 0);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId()
|
||||
{
|
||||
var result = await productService.AddProductCategoryAsync(1, 2, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task GetProductCategoriesAsync_ShouldReturn_ResultWithCategoryList()
|
||||
{
|
||||
var result = await productService.GetProductCategoriesAsync(1, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotEmpty(result.Value);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task DeleteProductCategoryAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var result = await productService.DeleteProductCategoryAsync(1, 1, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task DeleteAllProductCategoriesAsync_ShouldReturn_ResultWithSuccess()
|
||||
{
|
||||
var result = await productService.DeleteAllProductCategoriesAsync(1, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task GetProductPriceAsync_ShouldReturn_ResultOneProductPrice()
|
||||
{
|
||||
var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken);
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.NotNull(result.Value);
|
||||
|
||||
output.WriteLine(JsonSerializer.Serialize(result.Value));
|
||||
}
|
||||
|
||||
[IntegrationFact]
|
||||
public async Task GetProductPricesAsync_ShouldReturn_ResultProductPriceList()
|
||||
{
|
||||
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_ResultMatchingProducts()
|
||||
{
|
||||
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_ResultOneProduct()
|
||||
{
|
||||
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_ResultProducts()
|
||||
{
|
||||
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_ShouldResurn_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}");
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"ValidPayfastHosts": [
|
||||
"www.payfast.co.za",
|
||||
"sandbox.payfast.co.za",
|
||||
"w1w.payfast.co.za",
|
||||
"w2w.payfast.co.za",
|
||||
"ips.payfast.co.za",
|
||||
"api.payfast.co.za",
|
||||
"payment.payfast.io"
|
||||
],
|
||||
"HasherSettings": {
|
||||
"MinHashLength": 11
|
||||
},
|
||||
"BookshopS3Settings": {
|
||||
"ServiceUrl": "http://192.168.1.177:30900",
|
||||
"Region": "garage",
|
||||
"BucketName": "bookshop",
|
||||
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
|
||||
},
|
||||
"Email": {
|
||||
"Credentials": {
|
||||
"Username": "shop@litecharms.co.za"
|
||||
},
|
||||
"Port": 465,
|
||||
"Host": "mail.litecharms.co.za",
|
||||
"UseSsl": true
|
||||
},
|
||||
"Monitoring": {
|
||||
"ApiKey": "",
|
||||
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
|
||||
"ServiceName": "LiteCharms.LeadGenerator"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.AuthorBooks;
|
||||
|
||||
public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||
{
|
||||
public async ValueTask<Result> UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Book with ID {bookId} not found"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreateBookAsync(long authorId, long productId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken))
|
||||
return Result.Fail<long>("Author not found.");
|
||||
|
||||
if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken))
|
||||
return Result.Fail<long>("Product not found.");
|
||||
|
||||
var book = context.Books.Add(new Entities.AuthorBook
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
AuthorId = authorId,
|
||||
ProductId = productId
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(book.Entity.Id)
|
||||
: Result.Fail<long>("Failed to create book.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<AuthorBook>> GetBookAsync(long bookId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var book = await context.Books
|
||||
.AsNoTracking()
|
||||
.Include(b => b.Author)
|
||||
.Include(b => b.Product)
|
||||
.ThenInclude(b => b!.Prices)
|
||||
.Include(b => b.Pages)
|
||||
.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken);
|
||||
|
||||
return book is null
|
||||
? Result.Fail<AuthorBook>(new Error($"Book with ID {bookId} not found"))
|
||||
: Result.Ok(book.ToModel());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<AuthorBook>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<AuthorBook[]>> GetBooksByAuthorAsync(long authorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
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"));
|
||||
|
||||
var books = await context.Books
|
||||
.AsNoTracking()
|
||||
.Include(b => b.Author)
|
||||
.Include(b => b.Product)
|
||||
.ThenInclude(b => b!.Prices)
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.Where(b => b.AuthorId == authorId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return books?.Count > 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<AuthorBook[]>> GetPublishedBooksAsync(int offset, int limit, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var books = await context.Books
|
||||
.AsNoTracking()
|
||||
.Include(b => b.Author)
|
||||
.Include(b => b.Product)
|
||||
.ThenInclude(b => b!.Prices)
|
||||
.Include(b => b.Pages)
|
||||
.Where(b => b.Enabled && b.Product!.Enabled && b.Author!.Enabled)
|
||||
.OrderByDescending(b => b.Ranking)
|
||||
.ThenByDescending(b => b.Ranking)
|
||||
.ThenByDescending(b => b.CreatedAt)
|
||||
.ThenByDescending(b => b.UpdatedAt)
|
||||
.Skip(offset).Take(limit)
|
||||
.AsSplitQuery()
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return books?.Length > 0
|
||||
? Result.Ok(books.Select(b => b.ToModel()).ToArray())
|
||||
: Result.Fail<AuthorBook[]>(new Error("No published books found."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Authors.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Pages.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Products.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
|
||||
public class AuthorBook : Models.AuthorBook
|
||||
{
|
||||
public virtual Author? Author { get; set; }
|
||||
|
||||
public new virtual Product? Product { get; set; }
|
||||
|
||||
public virtual ICollection<BookPage> Pages { get; set; } = [];
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
|
||||
public sealed class AuthorBookConfiguration : IEntityTypeConfiguration<AuthorBook>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<AuthorBook> builder)
|
||||
{
|
||||
builder.ToTable("Books");
|
||||
|
||||
builder.HasKey(f => f.AuthorId);
|
||||
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.AuthorId).IsRequired();
|
||||
builder.Property(f => f.ProductId).IsRequired();
|
||||
builder.Property(f => f.Rating).IsRequired(false);
|
||||
builder.Property(f => f.Ranking).IsRequired(false);
|
||||
builder.Property(f => f.Enabled).HasDefaultValue(true);
|
||||
|
||||
builder.HasOne(f => f.Author)
|
||||
.WithMany(a => a.Books)
|
||||
.HasForeignKey(f => f.AuthorId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(f => f.Product)
|
||||
.WithMany()
|
||||
.HasForeignKey(f => f.ProductId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Products.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
||||
|
||||
public class AuthorBook
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public long AuthorId { get; set; }
|
||||
|
||||
public long ProductId { get; set; }
|
||||
|
||||
public int Rating { get; set; }
|
||||
|
||||
public int Ranking { get; set; }
|
||||
|
||||
public Product? Product { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Authors;
|
||||
|
||||
public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||
{
|
||||
public async ValueTask<Result<Author>> GetAuthorByProductIdAsync(long productId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var author = await context.Books
|
||||
.AsNoTracking()
|
||||
.Include(i => i.Author)
|
||||
.Where(b => b.ProductId == productId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (author is null)
|
||||
return Result.Fail<Author>(new Error($"No author association discovered for Product ID {productId}"));
|
||||
|
||||
return Result.Ok(author.Author!.ToModel());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Author>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Author with ID {authorId} not found"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Author>> GetAuthorAsync(long authorId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
|
||||
|
||||
return author is not null
|
||||
? Result.Ok(author.ToModel())
|
||||
: Result.Fail<Author>(new Error($"Author with ID {authorId} not found"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Author>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Author[]>> GetAuthorsAsync(DateRange range, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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 authors = await context.Authors.AsNoTracking()
|
||||
.OrderByDescending(o => o.CreatedAt)
|
||||
.ThenByDescending(o => o.UpdatedAt)
|
||||
.Where(a => a.CreatedAt >= fromDate && a.CreatedAt <= toDate)
|
||||
.Take(range.MaxRecords)
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return authors?.Length > 0
|
||||
? Result.Ok(authors.Select(a => a.ToModel()).ToArray())
|
||||
: Result.Fail<Author[]>(new Error("No authors found in the specified date range."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Author[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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.PublisherType = request.PublisherType;
|
||||
author.Company = request.Company;
|
||||
author.VatNumber = request.VatNumber;
|
||||
author.Name = request.Name;
|
||||
author.LastName = request.LastName;
|
||||
author.Biography = request.Biography;
|
||||
author.Email = request.Email;
|
||||
author.Website = request.Website;
|
||||
author.ImageUrl = request.ImageUrl;
|
||||
author.ThumbnailImageUrl = request.ThumbnailImageUrl;
|
||||
author.SocialMedia = request.SocialMedia;
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Failed to update author with ID {authorId}"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreateAuthorAsync(CreateAuthor 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<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))
|
||||
return Result.Fail<long>(new Error($"An author with the email {request.Email} already exists"));
|
||||
|
||||
var newAuthor = context.Authors.Add(new Entities.Author
|
||||
{
|
||||
Company = request.Company,
|
||||
VatNumber = request.VatNumber,
|
||||
PublisherType = request.PublisherType,
|
||||
Name = request.Name,
|
||||
LastName = request.LastName,
|
||||
Biography = request.Biography,
|
||||
Email = request.Email,
|
||||
Website = request.Website,
|
||||
ImageUrl = request.ImageUrl,
|
||||
ThumbnailImageUrl = request.ThumbnailImageUrl,
|
||||
SocialMedia = request.SocialMedia
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(newAuthor.Entity.Id)
|
||||
: Result.Fail<long>(new Error($"Failed to create author {request.Name} {request.LastName}"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Authors.Entities;
|
||||
|
||||
[EntityTypeConfiguration<AuthorConfiguration, Author>]
|
||||
public sealed class Author : Models.Author
|
||||
{
|
||||
public ICollection<AuthorBook> Books { get; set; } = [];
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Authors.Entities;
|
||||
|
||||
public sealed class AuthorConfiguration : IEntityTypeConfiguration<Author>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Author> builder)
|
||||
{
|
||||
builder.ToTable("Authors");
|
||||
|
||||
builder.HasKey(f => f.Id);
|
||||
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.PublisherType).IsRequired();
|
||||
builder.Property(f => f.VatNumber).IsRequired(false).HasMaxLength(255);
|
||||
builder.Property(f => f.Name).IsRequired().HasMaxLength(255);
|
||||
builder.Property(f => f.LastName).IsRequired().HasMaxLength(255);
|
||||
builder.Property(f => f.Biography).IsRequired(false).HasMaxLength(2048);
|
||||
builder.Property(f => f.Email).IsRequired().HasMaxLength(512);
|
||||
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.Enabled).HasDefaultValue(true);
|
||||
|
||||
builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); });
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||
|
||||
public class Author
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public PublisherTypes PublisherType { get; set; }
|
||||
|
||||
public string? Company { get; set; }
|
||||
|
||||
public string? VatNumber { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public string? Biography { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Website { get; set; }
|
||||
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
public string? ThumbnailImageUrl { get; set; }
|
||||
|
||||
public ICollection<SocialMedia>? SocialMedia { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||
|
||||
public sealed record UpdateAuthor : CreateAuthor;
|
||||
|
||||
public record CreateAuthor
|
||||
{
|
||||
public required PublisherTypes PublisherType { get; set; }
|
||||
|
||||
public string? Company { get; set; }
|
||||
|
||||
public string? VatNumber { get; set; }
|
||||
|
||||
public required string Name { get; set; }
|
||||
|
||||
public required string LastName { get; set; }
|
||||
|
||||
public string? Biography { get; set; }
|
||||
|
||||
public required string Email { get; set; }
|
||||
|
||||
public string? Website { get; set; }
|
||||
|
||||
public required string ImageUrl { get; set; }
|
||||
|
||||
public string? ThumbnailImageUrl { get; set; }
|
||||
|
||||
public SocialMedia[]? SocialMedia { get; set; }
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Categories;
|
||||
|
||||
public sealed class CategoryService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||
{
|
||||
public async ValueTask<Result> UpdateCategoryStatusAsync(long categoryId, bool enabled, bool isMain, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var rowsUpdated = await context.Categories
|
||||
.Where(c => c.Id == categoryId && c.Enabled)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(c => c.Enabled, enabled)
|
||||
.SetProperty(c => c.IsMain, isMain), cancellationToken);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Failed to update category"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Category>> GetCategoryAsync(long categoryId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var category = await context.Categories.AsNoTracking().FirstOrDefaultAsync(c => c.Id == categoryId, cancellationToken);
|
||||
|
||||
return category is not null
|
||||
? Result.Ok(category.ToModel())
|
||||
: Result.Fail<Category>("Failed to create new category");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Category>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Category[]>> GetCategoriesAsync(bool? isMain = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var query = context.Categories.AsNoTracking()
|
||||
.OrderByDescending(o => o.IsMain)
|
||||
.ThenByDescending(o => o.Id)
|
||||
.ThenBy(o => o.Name)
|
||||
.AsQueryable();
|
||||
|
||||
query = isMain is null
|
||||
? query.Where(c => c.Enabled).AsQueryable()
|
||||
: query.Where(c => c.Enabled && c.IsMain == isMain.Value);
|
||||
|
||||
var categories = await query.ToListAsync(cancellationToken);
|
||||
|
||||
return categories?.Count > 0
|
||||
? Result.Ok(categories.Select(c => c.ToModel()).ToArray())
|
||||
: Result.Fail<Category[]>("No categories found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Category[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> CreateCategoriesAsync(CancellationToken cancellationToken = default, params string[] categories)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
foreach (var category in categories)
|
||||
{
|
||||
if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken))
|
||||
continue;
|
||||
|
||||
context.Categories.Add(new Entities.Category
|
||||
{
|
||||
Name = category.Humanize(LetterCasing.Title),
|
||||
IsMain = false,
|
||||
Enabled = true,
|
||||
});
|
||||
}
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Failed to add any category in the list");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreateCategoryAsync(string category, bool isMain, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (await context.Categories.AnyAsync(c => EF.Functions.ILike(c.Name!, category!), cancellationToken))
|
||||
return Result.Fail($"Category '{category}' already exists");
|
||||
|
||||
var newCategory = context.Categories.Add(new Entities.Category
|
||||
{
|
||||
Name = StringHumanizeExtensions.Humanize(category, LetterCasing.Title),
|
||||
IsMain = isMain,
|
||||
Enabled = true,
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(newCategory.Entity.Id)
|
||||
: Result.Fail("Failed to create new category");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Categories.Entities;
|
||||
|
||||
[EntityTypeConfiguration<CategoryConfiguration, Category>]
|
||||
public sealed class Category : Models.Category;
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Categories.Entities;
|
||||
|
||||
public sealed class CategoryConfiguration : IEntityTypeConfiguration<Category>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Category> builder)
|
||||
{
|
||||
builder.ToTable("Categories");
|
||||
|
||||
builder.HasKey(c => c.Id);
|
||||
builder.Property(c => c.Name).IsRequired().HasMaxLength(15);
|
||||
builder.Property(c => c.IsMain).HasDefaultValue(false);
|
||||
builder.Property(c => c.Enabled).HasDefaultValue(true);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Categories.Models;
|
||||
|
||||
public class Category
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public bool IsMain { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,398 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers;
|
||||
|
||||
public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||
{
|
||||
public async ValueTask<Result<long>> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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
|
||||
{
|
||||
Company = request.Company,
|
||||
VatNumber = request.VatNumber,
|
||||
Email = request.Email,
|
||||
Website = request.Website,
|
||||
Phone = request.Phone,
|
||||
SocialMedia = request.SocialMedia,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(customer.Entity.Id)
|
||||
: Result.Fail<long>(new Error("Failed to create customer."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreateCustomerContactAsync(long customerId, CreateCustomerContact request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
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 && 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
|
||||
{
|
||||
CustomerId = customerId,
|
||||
Name = request.Name,
|
||||
Email = request.Email,
|
||||
Phone = request.Phone,
|
||||
LastName = request.LastName,
|
||||
Type = request.Type,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(contact.Entity.Id)
|
||||
: Result.Fail<long>(new Error("Failed to create customer contact."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreateCustomerAddressAsync(long customerId, CreateCustomerAddress request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
|
||||
return Result.Fail<long>(new Error($"Customer with ID '{customerId}' does not exist."));
|
||||
|
||||
var address = context.Addresses.Add(new Entities.Address
|
||||
{
|
||||
CustomerId = customerId,
|
||||
Street = request.Street,
|
||||
City = request.City,
|
||||
State = request.State,
|
||||
PostalCode = request.PostalCode,
|
||||
Country = request.Country,
|
||||
Type = request.Type,
|
||||
Enabled = true,
|
||||
BuildingType = request.BuildingType,
|
||||
IsPrimary = request.IsPrimary,
|
||||
Name = request.Name
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(address.Entity.Id)
|
||||
: Result.Fail<long>(new Error("Failed to create customer address."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateCustomerAsync(long customerId, UpdateCustomer request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken);
|
||||
|
||||
if (customer is null)
|
||||
return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist."));
|
||||
|
||||
customer.UpdatedAt = DateTime.UtcNow;
|
||||
customer.Company = request.Company;
|
||||
customer.VatNumber = request.VatNumber;
|
||||
customer.Email = request.Email;
|
||||
customer.Website = request.Website;
|
||||
customer.Phone = request.Phone;
|
||||
customer.SocialMedia = request.SocialMedia;
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error("Failed to update customer."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateCustomerContactAsync(long contactId, UpdateCustomerContact request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateCustomerAddressAsync(long addressId, UpdateCustomerAddress request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateCustomerStatusAsync(long customerId, bool enabled, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Customer with ID '{customerId}' does not exist."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateCustomerContactStatusAsync(long contactId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateCustomerAddressStatusAsync(long addressId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Customer[]>> GetCustomersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var customers = await context.Customers
|
||||
.AsNoTracking()
|
||||
.Include(c => c.Contacts)
|
||||
.Include(c => c.Addresses)
|
||||
.OrderByDescending(c => c.CreatedAt)
|
||||
.ThenByDescending(c => c.UpdatedAt)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return customers?.Count > 0
|
||||
? Result.Ok(customers.Select(c => c.ToModel()).ToArray())
|
||||
: Result.Fail<Customer[]>(new Error("No customers found."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Customer[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Contact[]>> GetCustomerContactsAsync(long customerId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
|
||||
return Result.Fail<Contact[]>(new Error($"Customer with ID '{customerId}' does not exist."));
|
||||
|
||||
var contacts = await context.Contacts
|
||||
.AsNoTracking()
|
||||
.Where(cc => cc.CustomerId == customerId)
|
||||
.OrderByDescending(cc => cc.CreatedAt)
|
||||
.ThenByDescending(cc => cc.UpdatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return contacts?.Count > 0
|
||||
? Result.Ok(contacts.Select(cc => cc.ToModel()).ToArray())
|
||||
: Result.Fail<Contact[]>(new Error("No contacts found for the specified customer."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Contact[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Address[]>> GetCustomerAddressesAsync(long customerId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
|
||||
return Result.Fail<Address[]>(new Error($"Customer with ID '{customerId}' does not exist."));
|
||||
|
||||
var addresses = await context.Addresses
|
||||
.AsNoTracking()
|
||||
.Where(a => a.CustomerId == customerId)
|
||||
.OrderByDescending(a => a.CreatedAt)
|
||||
.ThenByDescending(a => a.UpdatedAt)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return addresses?.Count > 0
|
||||
? Result.Ok(addresses.Select(a => a.ToModel()).ToArray())
|
||||
: Result.Fail<Address[]>(new Error($"No addresses found for customer with ID '{customerId}'."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Address[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Customer>> GetCustomerAsync(long customerId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var customer = await context.Customers
|
||||
.AsNoTracking()
|
||||
.Include(c => c.Contacts)
|
||||
.Include(c => c.Addresses)
|
||||
.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken);
|
||||
|
||||
return customer is not null
|
||||
? Result.Ok(customer.ToModel())
|
||||
: Result.Fail<Customer>(new Error($"Customer with ID '{customerId}' does not exist."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Customer>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Contact>> GetCustomerContactAsync(long contactId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var contact = await context.Contacts
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken);
|
||||
|
||||
return contact is not null
|
||||
? Result.Ok(contact.ToModel())
|
||||
: Result.Fail<Contact>(new Error($"Contact with ID '{contactId}' does not exist."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Contact>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Address>> GetCustomerAddressAsync(long addressId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var address = await context.Addresses
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken);
|
||||
|
||||
return address is not null
|
||||
? Result.Ok(address.ToModel())
|
||||
: Result.Fail<Address>(new Error($"Address with ID '{addressId}' does not exist."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Address>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
|
||||
[EntityTypeConfiguration<AddressConfiguration, Address>]
|
||||
public class Address : Models.Address
|
||||
{
|
||||
public virtual Customer? Customer { get; set; }
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
|
||||
public sealed class AddressConfiguration : IEntityTypeConfiguration<Address>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Address> builder)
|
||||
{
|
||||
builder.ToTable("Addresses");
|
||||
|
||||
builder.HasKey(a => a.Id);
|
||||
builder.Property(a => a.CustomerId).IsRequired();
|
||||
builder.Property(a => a.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(a => a.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(a => a.Name).IsRequired();
|
||||
builder.Property(a => a.Type).IsRequired();
|
||||
builder.Property(a => a.BuildingType).IsRequired();
|
||||
builder.Property(a => a.Street).IsRequired();
|
||||
builder.Property(a => a.City).IsRequired();
|
||||
builder.Property(a => a.State).IsRequired();
|
||||
builder.Property(a => a.PostalCode).IsRequired();
|
||||
builder.Property(a => a.Country).IsRequired();
|
||||
builder.Property(a => a.IsPrimary).HasDefaultValue(false);
|
||||
builder.Property(a => a.Enabled).HasDefaultValue(true);
|
||||
|
||||
builder.HasOne(a => a.Customer)
|
||||
.WithMany(c => c.Addresses)
|
||||
.HasForeignKey(a => a.CustomerId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ContactConfiguration, Contact>]
|
||||
public class Contact : Models.Contact
|
||||
{
|
||||
public virtual Customer? Customer { get; set; }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
|
||||
public sealed class ContactConfiguration : IEntityTypeConfiguration<Contact>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Contact> builder)
|
||||
{
|
||||
builder.ToTable("Contacts");
|
||||
|
||||
builder.HasKey(c => c.Id);
|
||||
builder.Property(c => c.CustomerId).IsRequired();
|
||||
builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(c => c.Name).IsRequired();
|
||||
builder.Property(c => c.LastName).IsRequired();
|
||||
builder.Property(c => c.Type).IsRequired();
|
||||
builder.Property(c => c.Phone).IsRequired();
|
||||
builder.Property(c => c.Email).IsRequired();
|
||||
builder.Property(c => c.IsPrimary).HasDefaultValue(false);
|
||||
builder.Property(c => c.Enabled).HasDefaultValue(true);
|
||||
|
||||
builder.HasOne(c => c.Customer)
|
||||
.WithMany(c => c.Contacts)
|
||||
.HasForeignKey(c => c.CustomerId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
|
||||
[EntityTypeConfiguration<CustomerConfiguration, Customer>]
|
||||
public class Customer : Models.Customer
|
||||
{
|
||||
public virtual ICollection<Contact> Contacts { get; set; } = [];
|
||||
|
||||
public virtual ICollection<Address> Addresses { get; set; } = [];
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
|
||||
public sealed class CustomerConfiguration : IEntityTypeConfiguration<Customer>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Customer> builder)
|
||||
{
|
||||
builder.ToTable("Customers");
|
||||
|
||||
builder.HasKey(c => c.Id);
|
||||
builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(c => c.Company).IsRequired(false);
|
||||
builder.Property(c => c.VatNumber).IsRequired(false);
|
||||
builder.Property(c => c.Email).IsRequired();
|
||||
builder.Property(c => c.Phone).IsRequired();
|
||||
builder.Property(c => c.Website).IsRequired();
|
||||
builder.Property(c => c.Enabled).HasDefaultValue(true);
|
||||
|
||||
builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); });
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||
|
||||
public class Address
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long CustomerId { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public AddressType Type { get; set; }
|
||||
|
||||
public AddressBuildingTypes BuildingType { get; set; }
|
||||
|
||||
public string? Street { get; set; }
|
||||
|
||||
public string? City { get; set; }
|
||||
|
||||
public string? State { get; set; }
|
||||
|
||||
public string? PostalCode { get; set; }
|
||||
|
||||
public string? Country { get; set; }
|
||||
|
||||
public bool IsPrimary { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||
|
||||
public class Contact
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long CustomerId { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public ContactTypes Type { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Phone { get; set; }
|
||||
|
||||
public bool IsPrimary { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||
|
||||
public class Customer
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public string? Company { get; set; }
|
||||
|
||||
public string? VatNumber { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Website { get; set; }
|
||||
|
||||
public string? Phone { get; set; }
|
||||
|
||||
public ICollection<SocialMedia>? SocialMedia { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||
|
||||
public record CreateCustomer
|
||||
{
|
||||
public string? Company { get; set; }
|
||||
|
||||
public string? VatNumber { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Website { get; set; }
|
||||
|
||||
public string? Phone { get; set; }
|
||||
|
||||
public ICollection<SocialMedia>? SocialMedia { get; set; }
|
||||
}
|
||||
|
||||
public sealed record UpdateCustomer : CreateCustomer;
|
||||
|
||||
public record CreateCustomerContact
|
||||
{
|
||||
public ContactTypes Type { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Phone { get; set; }
|
||||
}
|
||||
|
||||
public sealed record UpdateCustomerContact : CreateCustomerContact;
|
||||
|
||||
public record CreateCustomerAddress
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public AddressType Type { get; set; }
|
||||
|
||||
public AddressBuildingTypes BuildingType { get; set; }
|
||||
|
||||
public string? Street { get; set; }
|
||||
|
||||
public string? City { get; set; }
|
||||
|
||||
public string? State { get; set; }
|
||||
|
||||
public string? PostalCode { get; set; }
|
||||
|
||||
public string? Country { get; set; }
|
||||
|
||||
public bool IsPrimary { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
|
||||
public sealed record UpdateCustomerAddress : CreateCustomerAddress;
|
||||
@@ -1,30 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.HealthChecks;
|
||||
using static LiteCharms.Features.Extensions.Postgres;
|
||||
using static LiteCharms.Features.MidrandBooks.Extensions.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
public static class HealthChecks
|
||||
{
|
||||
public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services)
|
||||
{
|
||||
services.AddHealthChecks().AddCheck<MidrandShopQuartzHealthCheck>(SchedulerDbConfigName);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services)
|
||||
{
|
||||
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandBooksDbConfigName);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddHealthChecks()
|
||||
.AddCheck("Self", () => HealthCheckResult.Healthy());
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Products.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
public static class Mappers
|
||||
{
|
||||
public static PaymentGatewayLedger ToModel(this Payments.Entities.PaymentGatewayLedger entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
CustomerEmail = entity.CustomerEmail,
|
||||
OrderId = entity.OrderId,
|
||||
PaymentId = entity.PaymentId,
|
||||
MerchantPaymentId = entity.MerchantPaymentId,
|
||||
PayfastPaymentId = entity.PayfastPaymentId,
|
||||
PaymentStatus = entity.PaymentStatus,
|
||||
AmountGross = entity.AmountGross,
|
||||
AmountFee = entity.AmountFee,
|
||||
AmountNet = entity.AmountNet
|
||||
};
|
||||
|
||||
public static Refund ToModel(this Payments.Entities.Refund entity) => new()
|
||||
{
|
||||
CreatedAt = entity.CreatedAt,
|
||||
Amount = entity.Amount,
|
||||
Id = entity.Id,
|
||||
OrderId = entity.OrderId,
|
||||
Reason = entity.Reason,
|
||||
Status = entity.Status,
|
||||
Type = entity.Type,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
};
|
||||
|
||||
public static PaymentLedger ToModel(this Payments.Entities.PaymentLedger entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
CustomerId = entity.CustomerId,
|
||||
OrderId = entity.OrderId,
|
||||
PaymentId = entity.PaymentId,
|
||||
Status = entity.Status,
|
||||
MerchantPaymentId = entity.MerchantPaymentId,
|
||||
};
|
||||
|
||||
public static PaymentGateway ToModel(this Payments.Entities.PaymentGateway entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
Enabled = entity.Enabled,
|
||||
IsSandbox = entity.IsSandbox,
|
||||
MerchantId = entity.MerchantId,
|
||||
MerchantKey = entity.MerchantKey,
|
||||
Name = entity.Name,
|
||||
Website = entity.Website,
|
||||
};
|
||||
|
||||
public static Payment ToModel(this Payments.Entities.Payment entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
Amount = entity.Amount,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
OrderId = entity.OrderId,
|
||||
Reference = entity.Reference,
|
||||
Status = entity.Status,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
};
|
||||
|
||||
public static ProductInventory ToModel(this Products.Entities.ProductInventory entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
ProductId = entity.ProductId,
|
||||
ProductPriceId = entity.ProductPriceId,
|
||||
Status = entity.Status,
|
||||
TotalAllocated = entity.TotalAllocated,
|
||||
TotalReserved = entity.TotalReserved,
|
||||
};
|
||||
|
||||
public static Category ToModel(this Categories.Entities.Category entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
Name = entity.Name,
|
||||
IsMain = entity.IsMain,
|
||||
Enabled = entity.Enabled,
|
||||
};
|
||||
|
||||
public static ShippingProvider ToModel(this Orders.Entities.ShippingProvider entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
Name = entity.Name,
|
||||
Type = entity.Type,
|
||||
Price = entity.Price,
|
||||
Enabled = entity.Enabled,
|
||||
TrackingUrl = entity.TrackingUrl,
|
||||
};
|
||||
|
||||
public static Shipping ToModel(this Orders.Entities.Shipping entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
OrderId = entity.OrderId,
|
||||
AddressId = entity.AddressId,
|
||||
Status = entity.Status,
|
||||
TrackingNumber = entity.TrackingNumber
|
||||
};
|
||||
|
||||
public static OrderItem ToModel(this Orders.Entities.OrderItem entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
OrderId = entity.OrderId,
|
||||
Quantity = entity.Quantity,
|
||||
AuthorBookId = entity.AuthorBookId,
|
||||
ProductPriceId = entity.ProductPriceId
|
||||
};
|
||||
|
||||
public static Order ToModel(this Orders.Entities.Order entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CustomerId = entity.CustomerId,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
Status = entity.Status,
|
||||
Total = entity.Total,
|
||||
InvoiceUrl = entity.InvoiceUrl,
|
||||
Notes = entity.Notes
|
||||
};
|
||||
|
||||
public static Customer ToModel(this Customers.Entities.Customer entiry) => new()
|
||||
{
|
||||
Id = entiry.Id,
|
||||
Company = entiry.Company,
|
||||
CreatedAt = entiry.CreatedAt,
|
||||
Email = entiry.Email,
|
||||
Enabled = entiry.Enabled,
|
||||
Phone = entiry.Phone,
|
||||
SocialMedia = entiry.SocialMedia,
|
||||
UpdatedAt = entiry.UpdatedAt,
|
||||
VatNumber = entiry.VatNumber,
|
||||
Website = entiry.Website
|
||||
};
|
||||
|
||||
public static Address ToModel(this Customers.Entities.Address entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
BuildingType = entity.BuildingType,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
CustomerId = entity.CustomerId,
|
||||
Enabled = entity.Enabled,
|
||||
IsPrimary = entity.IsPrimary,
|
||||
Name = entity.Name,
|
||||
PostalCode = entity.PostalCode,
|
||||
Type = entity.Type,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
Street = entity.Street,
|
||||
City = entity.City,
|
||||
State = entity.State,
|
||||
Country = entity.Country
|
||||
};
|
||||
|
||||
public static Contact ToModel(this Customers.Entities.Contact entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
Type = entity.Type,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
CustomerId = entity.CustomerId,
|
||||
Email = entity.Email,
|
||||
Enabled = entity.Enabled,
|
||||
LastName = entity.LastName,
|
||||
Name = entity.Name,
|
||||
Phone = entity.Phone
|
||||
};
|
||||
|
||||
public static BookPage ToModel(this Pages.Entities.BookPage entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
AuthorBookId = entity.AuthorBookId,
|
||||
Content = entity.Content,
|
||||
ContentType = entity.ContentType,
|
||||
Number = entity.Number,
|
||||
Enabled = entity.Enabled,
|
||||
Notes = entity.Notes,
|
||||
References = entity.References,
|
||||
Type = entity.Type
|
||||
};
|
||||
|
||||
public static AuthorBook ToModel(this AuthorBooks.Entities.AuthorBook entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
ProductId = entity.ProductId,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
AuthorId = entity.AuthorId,
|
||||
Ranking = entity.Ranking,
|
||||
Rating = entity.Rating,
|
||||
Enabled = entity.Enabled,
|
||||
Product = entity.Product?.ToModel(),
|
||||
};
|
||||
|
||||
public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
ProductId = entity.ProductId,
|
||||
Amount = entity.Amount,
|
||||
Discount = entity.Discount,
|
||||
Enabled = entity.Enabled
|
||||
};
|
||||
|
||||
public static Product ToModel(this Products.Entities.Product entity) => new Product
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
Name = entity.Name,
|
||||
Summary = entity.Summary,
|
||||
Description = entity.Description,
|
||||
Type = entity.Type,
|
||||
ImageUrl = entity.ImageUrl,
|
||||
ThumbnailUrls = entity.ThumbnailUrls,
|
||||
Metadata = entity.Metadata,
|
||||
Enabled = entity.Enabled,
|
||||
Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null,
|
||||
};
|
||||
|
||||
public static Author ToModel(this Authors.Entities.Author entity) => new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
Name = entity.Name,
|
||||
LastName = entity.LastName,
|
||||
Biography = entity.Biography,
|
||||
Email = entity.Email,
|
||||
Website = entity.Website,
|
||||
ImageUrl = entity.ImageUrl,
|
||||
ThumbnailImageUrl = entity.ThumbnailImageUrl,
|
||||
SocialMedia = entity.SocialMedia,
|
||||
Enabled = entity.Enabled,
|
||||
Company = entity.Company,
|
||||
PublisherType = entity.PublisherType,
|
||||
VatNumber = entity.VatNumber
|
||||
};
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
public static class Postgres
|
||||
{
|
||||
public const string MidrandBooksDbConfigName = "PostgresMidrandBooks";
|
||||
|
||||
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(dataSource));
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
public static class Shop
|
||||
{
|
||||
public static IServiceCollection AddShopServices(this IServiceCollection services)
|
||||
{
|
||||
var serviceType = typeof(IService);
|
||||
|
||||
var implementations = Assembly.GetExecutingAssembly().GetTypes()
|
||||
.Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
|
||||
|
||||
foreach (var implementation in implementations)
|
||||
services.AddScoped(implementation);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using static LiteCharms.Features.Extensions.Quartz;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.HealthChecks;
|
||||
|
||||
public sealed class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
|
||||
{
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var scheduler = await schedulerFactory.GetScheduler(MidrandShopSchedulerName, cancellationToken);
|
||||
|
||||
if(scheduler == null)
|
||||
return HealthCheckResult.Unhealthy($"Scheduler with name '{MidrandShopSchedulerName}' not found.");
|
||||
|
||||
if (!scheduler.IsStarted)
|
||||
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler is not running");
|
||||
|
||||
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
|
||||
|
||||
return HealthCheckResult.Healthy($"{MidrandShopSchedulerName} Quartz scheduler is ready");
|
||||
}
|
||||
catch (SchedulerException)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler cannot connect to the store");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using static LiteCharms.Features.MidrandBooks.Extensions.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.HealthChecks;
|
||||
|
||||
public sealed class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck
|
||||
{
|
||||
private readonly string connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName)!;
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var dataSource = NpgsqlDataSource.Create(connectionString);
|
||||
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT 1";
|
||||
|
||||
await command.ExecuteScalarAsync(cancellationToken);
|
||||
|
||||
return HealthCheckResult.Healthy($"{MidrandBooksDbConfigName} is responsive.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy($"{MidrandBooksDbConfigName} is unreachable.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
|
||||
<UserSecretsId>5be62f49-3ed0-4468-884e-1b04e048b45a</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Nuget Package Details -->
|
||||
<PropertyGroup>
|
||||
<PackageId>LiteCharms.Features.MidrandBooks</PackageId>
|
||||
<Version>1.0.20</Version>
|
||||
<Authors>Khwezi Mngoma</Authors>
|
||||
<Company>Lite Charms (PTY) Ltd</Company>
|
||||
<Description>MidrandBooks feature components for Lite Charms applications.</Description>
|
||||
<PackageProjectUrl>https://gitea.khongisa.co.za/litecharms/components</PackageProjectUrl>
|
||||
<RepositoryUrl>https://gitea.khongisa.co.za/litecharms/components.git</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageTags>utility;dotnet</PackageTags>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\LICENSE" Pack="true" PackagePath="\" />
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Quartz Scheduler-->
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Humanizer" Version="3.0.10" />
|
||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.98">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<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.3" />
|
||||
<PackageReference Include="AWSSDK.S3" Version="4.0.24" />
|
||||
<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="System.Net.Sockets" />
|
||||
<Using Include="System.Text.RegularExpressions" />
|
||||
<Using Include="System.Web" />
|
||||
<Using Include="System.Net" />
|
||||
<Using Include="Humanizer" />
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
@@ -1,13 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
[EntityTypeConfiguration<OrderConfiguration, Order>]
|
||||
public class Order : Models.Order
|
||||
{
|
||||
public virtual Shipping? Shipping { get; set; }
|
||||
|
||||
public virtual ICollection<OrderItem> OrderItems { get; set; } = [];
|
||||
|
||||
public virtual ICollection<Refund> Refunds { get; set; } = [];
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Order> builder)
|
||||
{
|
||||
builder.ToTable("Orders");
|
||||
|
||||
builder.HasKey(o => o.Id);
|
||||
builder.Property(o => o.CustomerId).IsRequired();
|
||||
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().HasPrecision(18, 2);
|
||||
builder.Property(o => o.Notes).HasMaxLength(1000);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Products.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
[EntityTypeConfiguration<OrderItemConfiguration, OrderItem>]
|
||||
public class OrderItem : Models.OrderItem
|
||||
{
|
||||
public virtual Order? Order { get; set; }
|
||||
|
||||
public virtual AuthorBook? AuthorBook { get; set; }
|
||||
|
||||
public virtual ProductPrice? ProductPrice { get; set; }
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
public sealed class OrderItemConfiguration : IEntityTypeConfiguration<OrderItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OrderItem> builder)
|
||||
{
|
||||
builder.ToTable("OrderItems");
|
||||
|
||||
builder.HasKey(oi => oi.Id);
|
||||
builder.Property(oi => oi.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(oi => oi.OrderId).IsRequired();
|
||||
builder.Property(oi => oi.AuthorBookId).IsRequired();
|
||||
builder.Property(oi => oi.ProductPriceId).IsRequired();
|
||||
builder.Property(oi => oi.Quantity).IsRequired();
|
||||
|
||||
builder.HasOne(oi => oi.Order)
|
||||
.WithMany(o => o.OrderItems)
|
||||
.HasForeignKey(oi => oi.OrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(oi => oi.AuthorBook)
|
||||
.WithMany()
|
||||
.HasForeignKey(oi => oi.AuthorBookId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(oi => oi.ProductPrice)
|
||||
.WithMany()
|
||||
.HasForeignKey(oi => oi.ProductPriceId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ShippingConfiguration, Shipping>]
|
||||
public class Shipping : Models.Shipping
|
||||
{
|
||||
public virtual Order? Order { get; set; }
|
||||
|
||||
public virtual Address? Address { get; set; }
|
||||
|
||||
public virtual ShippingProvider? ShippingProvider { get; set; }
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
public sealed class ShippingConfiguration : IEntityTypeConfiguration<Shipping>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Shipping> builder)
|
||||
{
|
||||
builder.ToTable("Shippings");
|
||||
|
||||
builder.HasKey(s => s.Id);
|
||||
builder.Property(s => s.OrderId).IsRequired();
|
||||
builder.Property(s => s.AddressId).IsRequired();
|
||||
builder.Property(s => s.ShippingProviderId).IsRequired();
|
||||
builder.Property(s => s.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(s => s.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(s => s.Status).IsRequired();
|
||||
builder.Property(s => s.TrackingNumber).HasMaxLength(255);
|
||||
|
||||
builder.HasOne(s => s.Order)
|
||||
.WithOne(o => o.Shipping)
|
||||
.HasForeignKey<Shipping>(s => s.OrderId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(s => s.Address)
|
||||
.WithMany()
|
||||
.HasForeignKey(s => s.AddressId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
builder.HasOne(f => f.ShippingProvider)
|
||||
.WithMany(f => f.Shippings)
|
||||
.HasForeignKey(f => f.ShippingProviderId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
public class ShippingProvider : Models.ShippingProvider
|
||||
{
|
||||
public virtual ICollection<Shipping> Shippings { get; set; } = [];
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
public sealed class ShippingProviderConfiguration : IEntityTypeConfiguration<ShippingProvider>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<ShippingProvider> builder)
|
||||
{
|
||||
builder.ToTable("ShippingProviders");
|
||||
|
||||
builder.HasKey(f => f.Id);
|
||||
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.Type).IsRequired();
|
||||
builder.Property(f => f.Name).IsRequired().HasMaxLength(100);
|
||||
builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2);
|
||||
builder.Property(f => f.Enabled).HasDefaultValue(true);
|
||||
builder.Property(f => f.TrackingUrl).HasMaxLength(200);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
|
||||
public class Order
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public long CustomerId { get; set; }
|
||||
|
||||
public OrderStatus Status { get; set; }
|
||||
|
||||
public decimal Total { get; set; }
|
||||
|
||||
public string? Notes { get; set; }
|
||||
|
||||
public string? InvoiceUrl { get; set; }
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
|
||||
public class OrderItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public long OrderId { get; set; }
|
||||
|
||||
public long AuthorBookId { get; set; }
|
||||
|
||||
public long ProductPriceId { get; set; }
|
||||
|
||||
public int Quantity { get; set; }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
|
||||
public sealed record CreateOrder(decimal TotalPrice, string? Notes);
|
||||
|
||||
public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity);
|
||||
|
||||
public sealed record CreateShipping(long AddressId, long ShippingProviderId, string? TrackingNumber = null);
|
||||
|
||||
public sealed record CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl);
|
||||
|
||||
public sealed record UpdateShippingProvider(long ProviderId, bool Enabled, string Name, decimal Price, string TrackingUrl);
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
|
||||
public class Shipping
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public long OrderId { get; set; }
|
||||
|
||||
public long AddressId { get; set; }
|
||||
|
||||
public long ShippingProviderId { get; set; }
|
||||
|
||||
public string? TrackingNumber { get; set; }
|
||||
|
||||
public ShippingStatuses Status { get; set; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
|
||||
public class ShippingProvider
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public ShippingProviderTypes Type { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public decimal? Price { get; set; }
|
||||
|
||||
public string? TrackingUrl { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,462 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Orders;
|
||||
|
||||
public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||
{
|
||||
public async ValueTask<Result<long>> CreateOrderAsync(long customerId, CreateOrder request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
|
||||
return Result.Fail<long>("Customer not found.");
|
||||
|
||||
var order = context.Orders.Add(new Entities.Order
|
||||
{
|
||||
CustomerId = customerId,
|
||||
Status = OrderStatus.Pending,
|
||||
Total = request.TotalPrice
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(order.Entity.Id)
|
||||
: Result.Fail<long>("Failed to create order.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> AddItemToOrderAsync(long orderId, CreateOrderItem request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken))
|
||||
return Result.Fail<long>("Order not found.");
|
||||
|
||||
if(!await context.Books.AnyAsync(ab => ab.Id == request.AuthorBookId, cancellationToken))
|
||||
return Result.Fail<long>("Author book not found.");
|
||||
|
||||
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,
|
||||
AuthorBookId = request.AuthorBookId,
|
||||
ProductPriceId = request.ProductPriceId,
|
||||
Quantity = request.Quantity
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(orderItem.Entity.Id)
|
||||
: Result.Fail<long>("Failed to add item to order.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> AddItemsToOrderAsync(long orderId, CreateOrderItem[] items, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
if(items.Length == 0)
|
||||
return Result.Fail("No items to add.");
|
||||
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken))
|
||||
return Result.Fail("Order not found.");
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (!await context.Books.AnyAsync(ab => ab.Id == item.AuthorBookId, cancellationToken))
|
||||
return Result.Fail($"Author book with ID {item.AuthorBookId} not found.");
|
||||
|
||||
if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken))
|
||||
return Result.Fail($"Product price with ID {item.ProductPriceId} not found.");
|
||||
|
||||
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 Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> RemoveItemFromOrderAsync(long orderId, long orderItemId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var rowsDeleted = await context.OrderItems
|
||||
.Where(oi => oi.Id == orderItemId && oi.OrderId == orderId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
return rowsDeleted > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Order item not found or failed to remove.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> ClearOrderItemsAsync(long orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var deletedItems = await context.OrderItems.Where(oi => oi.OrderId == orderId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Failed to clear order items.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> CancelOrderAsync(long orderId, CancellationToken cancellationToken = default) =>
|
||||
await UpdateOrderStatusAsync(orderId, OrderStatus.Cancelled, cancellationToken);
|
||||
|
||||
public async ValueTask<Result<Order>> GetOrderAsync(long orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var order = await context.Orders.AsNoTracking().FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
|
||||
|
||||
return order is not null
|
||||
? Result.Ok(order.ToModel())
|
||||
: Result.Fail<Order>("Order not found.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Order>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Order[]>> GetOrdersByCustomerAsync(long customerId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if(!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
|
||||
return Result.Fail<Order[]>("Customer not found.");
|
||||
|
||||
var orders = await context.Orders
|
||||
.AsNoTracking()
|
||||
.Where(o => o.CustomerId == customerId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(orders.Select(o => o.ToModel()).ToArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Order[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Order[]>> GetOrdersAsync(DateRange range, int index, CancellationToken cancellationToken = default)
|
||||
{
|
||||
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 >= fromDate && o.CreatedAt <= toDate)
|
||||
.Skip(index).Take(range.MaxRecords)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(orders.Select(o => o.ToModel()).ToArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Order[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateOrderStatusAsync(long orderId, OrderStatus newStatus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Order not found or status update failed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken))
|
||||
return Result.Fail("Order not found.");
|
||||
|
||||
if(!await context.Addresses.AnyAsync(a => a.Id == request.AddressId, cancellationToken))
|
||||
return Result.Fail("Address not found.");
|
||||
|
||||
if(!await context.ShippingProviders.AnyAsync(sp => sp.Id == request.ShippingProviderId && sp.Enabled, cancellationToken))
|
||||
return Result.Fail("Shipping provider not found or disabled.");
|
||||
|
||||
if(await context.Shippings.AnyAsync(s => s.OrderId == orderId, cancellationToken))
|
||||
return Result.Fail("Shipping already exists for this order.");
|
||||
|
||||
var shipping = context.Shippings.Add(new Entities.Shipping
|
||||
{
|
||||
OrderId = orderId,
|
||||
AddressId = request.AddressId,
|
||||
ShippingProviderId = request.ShippingProviderId,
|
||||
Status = ShippingStatuses.Pending,
|
||||
TrackingNumber = request.TrackingNumber
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Failed to add shipping to order.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateShippingStatusAsync(long orderId, ShippingStatuses newStatus, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Shipping not found for this order or status update failed.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<Shipping>> GetShippingByOrderIdAsync(long orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var shipping = await context.Shippings
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken);
|
||||
|
||||
return shipping is not null
|
||||
? Result.Ok(shipping.ToModel())
|
||||
: Result.Fail<Shipping>("Shipping not found for this order.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Shipping>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> RemoveShippingFromOrderAsync(long orderId, long shippingId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var rowsDeleted = await context.Shippings
|
||||
.Where(s => s.Id == shippingId && s.OrderId == orderId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
return rowsDeleted > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Shipping record not found for this order.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateShippingTrackingNumberAsync(long orderId, long shippingId, string trackingNumber, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Shipping record not found for this order.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if(await context.ShippingProviders.AnyAsync(sp => sp.Type == request.Type, cancellationToken))
|
||||
return Result.Fail("Shipping provider with the same type already exists.");
|
||||
|
||||
var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Name = request.Name,
|
||||
Type = request.Type,
|
||||
Price = request.Price,
|
||||
TrackingUrl = request.TrackingUrl
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(shippingProvider.Entity.Id)
|
||||
: Result.Fail("Failed to create shipping provider.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<ShippingProvider[]>> GetShippingProvidersAsync(bool isEnabled, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var providers = await context.ShippingProviders.AsNoTracking().Where(sp => sp.Enabled == isEnabled)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return Result.Ok(providers.Select(sp => sp.ToModel()).ToArray());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<ShippingProvider[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<ShippingProvider>> GetShippingProviderAsync(long providerId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var provider = await context.ShippingProviders.AsNoTracking()
|
||||
.FirstOrDefaultAsync(sp => sp.Id == providerId, cancellationToken);
|
||||
|
||||
return provider is not null
|
||||
? Result.Ok(provider.ToModel())
|
||||
: Result.Fail<ShippingProvider>("Shipping provider not found.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<ShippingProvider>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateShippingProviderAsync(UpdateShippingProvider request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Shipping provider not found.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages.Entities;
|
||||
|
||||
[EntityTypeConfiguration<BookPageConfiguration, BookPage>]
|
||||
public class BookPage : Models.BookPage
|
||||
{
|
||||
public virtual AuthorBook Book { get; set; } = new();
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages.Entities;
|
||||
|
||||
public sealed class BookPageConfiguration : IEntityTypeConfiguration<BookPage>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BookPage> builder)
|
||||
{
|
||||
builder.ToTable("BookPages");
|
||||
|
||||
builder.HasKey(bp => bp.Id);
|
||||
builder.Property(bp => bp.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(bp => bp.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(bp => bp.Number).IsRequired().HasDefaultValue(0);
|
||||
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.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)
|
||||
.HasForeignKey(f => f.AuthorBookId)
|
||||
.OnDelete(DeleteBehavior.NoAction);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||
|
||||
public class BookPage
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long AuthorBookId { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public BookPageTypes Type { get; set; }
|
||||
|
||||
public BookContentTypes ContentType { get; set; }
|
||||
|
||||
public int Number { get; set; }
|
||||
|
||||
public byte[]? Content { get; set; }
|
||||
|
||||
public string[]? Notes { get; set; }
|
||||
|
||||
public ICollection<PageReference>? References { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||
|
||||
public class CreateBookPage
|
||||
{
|
||||
public BookPageTypes Type { get; set; }
|
||||
|
||||
public BookContentTypes ContentType { get; set; }
|
||||
|
||||
public int Number { get; set; }
|
||||
|
||||
public byte[]? Content { get; set; }
|
||||
|
||||
public string[]? Notes { get; set; }
|
||||
|
||||
public ICollection<PageReference>? References { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateBookPage : CreateBookPage;
|
||||
@@ -1,210 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages;
|
||||
|
||||
public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||
{
|
||||
public async ValueTask<Result> DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var rowsDeleted = await context.Pages
|
||||
.Where(p => p.AuthorBookId == authorBookId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
return rowsDeleted > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("No pages found for the specified book");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> DeleteByPageTypeAsync(long authorBookId, int pageNumber, BookPageTypes pageType, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var rowsDeleted = await context.Pages
|
||||
.Where(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
return rowsDeleted > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Page not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdatePageStatusAsync(long bookPageId, bool enabled, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Page not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> DeletePageAsync(long bookPageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var rowsDeleted = await context.Pages
|
||||
.Where(p => p.Id == bookPageId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
return rowsDeleted > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Page not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdatePageAsync(long bookPageId, UpdateBookPage request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(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);
|
||||
|
||||
return rowsUpdated > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Page not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreatePageAsync(long authorBookId, CreateBookPage request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken))
|
||||
return Result.Fail<long>("Book not found");
|
||||
|
||||
if (await context.Pages.AnyAsync(p => p.AuthorBookId == authorBookId && p.Number == request.Number && p.Type == request.Type, cancellationToken))
|
||||
return Result.Fail<long>("A page with the same number already exists for this book");
|
||||
|
||||
var page = context.Pages.Add(new Entities.BookPage
|
||||
{
|
||||
AuthorBookId = authorBookId,
|
||||
Type = request.Type,
|
||||
ContentType = request.ContentType,
|
||||
Number = request.Number,
|
||||
Content = request.Content,
|
||||
Notes = request.Notes,
|
||||
References = request.References,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(page.Entity.Id)
|
||||
: Result.Fail<long>("Failed to create page");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<BookPage[]>> GetPagesAsync(long authorBookId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken))
|
||||
return Result.Fail<BookPage[]>("Book not found");
|
||||
|
||||
var pages = await context.Pages.AsNoTracking()
|
||||
.Where(p => p.AuthorBookId == authorBookId).ToArrayAsync(cancellationToken);
|
||||
|
||||
return pages?.Length > 0
|
||||
? Result.Ok(pages.Select(p => p.ToModel()).ToArray())
|
||||
: Result.Fail<BookPage[]>("No pages found for the specified book");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<BookPage[]>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<BookPage>> GetPageByNumberAsync(long pageId, int number, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId && p.Number == number, cancellationToken);
|
||||
|
||||
return page is not null
|
||||
? page.ToModel()
|
||||
: Result.Fail<BookPage>("Page not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<BookPage>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<BookPage>> GetPageAsync(long pageId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId, cancellationToken);
|
||||
|
||||
return page is not null
|
||||
? page.ToModel()
|
||||
: Result.Fail<BookPage>("Page not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<BookPage>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
[EntityTypeConfiguration<PaymentConfiguration, Payment>]
|
||||
public class Payment : Models.Payment
|
||||
{
|
||||
public virtual Order? Order { get; set; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
public sealed class PaymentConfiguration : IEntityTypeConfiguration<Payment>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Payment> builder)
|
||||
{
|
||||
builder.ToTable("Payments");
|
||||
|
||||
builder.HasKey(f => f.Id);
|
||||
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.UpdatedAt);
|
||||
builder.Property(f => f.Status).IsRequired();
|
||||
builder.Property(f => f.Reference).IsRequired();
|
||||
builder.Property(f => f.OrderId).IsRequired();
|
||||
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
|
||||
|
||||
builder.HasOne(f => f.Order)
|
||||
.WithMany()
|
||||
.HasForeignKey(f => f.OrderId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
[EntityTypeConfiguration<PaymentGatewayConfiguration, PaymentGateway>]
|
||||
public class PaymentGateway : Models.PaymentGateway;
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration<PaymentGateway>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PaymentGateway> builder)
|
||||
{
|
||||
builder.ToTable("Gateways");
|
||||
|
||||
builder.HasKey(f => f.Id);
|
||||
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.UpdatedAt);
|
||||
builder.Property(f => f.Website).IsRequired(false);
|
||||
builder.Property(f => f.IsSandbox);
|
||||
builder.Property(f => f.MerchantKey).IsRequired();
|
||||
builder.Property(f => f.MerchantId).IsRequired();
|
||||
builder.Property(f => f.Enabled);
|
||||
builder.Property(f => f.Name).IsRequired();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
[EntityTypeConfiguration<PaymentGatewayLedgerConfiguration, PaymentGatewayLedger>]
|
||||
public class PaymentGatewayLedger : Models.PaymentGatewayLedger
|
||||
{
|
||||
public virtual Order? Order { get; set; }
|
||||
|
||||
public virtual Payment? Payment { get; set; }
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
public sealed class PaymentGatewayLedgerConfiguration : IEntityTypeConfiguration<PaymentGatewayLedger>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PaymentGatewayLedger> builder)
|
||||
{
|
||||
builder.ToTable("GatewayLedger");
|
||||
|
||||
builder.HasKey(f => f.Id);
|
||||
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.OrderId).IsRequired();
|
||||
builder.Property(f => f.PaymentId).IsRequired();
|
||||
builder.Property(f => f.PayfastPaymentId).IsRequired();
|
||||
builder.Property(f => f.MerchantPaymentId).IsRequired();
|
||||
builder.Property(f => f.AmountGross).IsRequired().HasPrecision(18, 2);
|
||||
builder.Property(f => f.AmountFee).IsRequired().HasPrecision(18, 2);
|
||||
builder.Property(f => f.AmountNet).IsRequired().HasPrecision(18, 2);
|
||||
builder.Property(f => f.CustomerEmail).IsRequired(false);
|
||||
|
||||
builder.HasOne(f => f.Order)
|
||||
.WithMany()
|
||||
.HasForeignKey(f => f.OrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(f => f.Payment)
|
||||
.WithMany()
|
||||
.HasForeignKey(f => f.PaymentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
[EntityTypeConfiguration<PaymentLedgerConfiguration, PaymentLedger>]
|
||||
public class PaymentLedger : Models.PaymentLedger
|
||||
{
|
||||
public virtual Payment? Payment { get; set; }
|
||||
|
||||
public virtual Order? Order { get; set; }
|
||||
|
||||
public virtual Customer? Customer { get; set; }
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
public sealed class PaymentLedgerConfiguration : IEntityTypeConfiguration<PaymentLedger>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<PaymentLedger> builder)
|
||||
{
|
||||
builder.ToTable("Ledger");
|
||||
|
||||
builder.HasKey(f => f.Id);
|
||||
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(f => f.Status).IsRequired();
|
||||
builder.Property(f => f.MerchantPaymentId).IsRequired(false);
|
||||
builder.Property(f => f.OrderId).IsRequired();
|
||||
builder.Property(f => f.CustomerId).IsRequired();
|
||||
builder.Property(f => f.PaymentId).IsRequired();
|
||||
|
||||
builder.HasOne(f => f.Payment)
|
||||
.WithMany()
|
||||
.IsRequired()
|
||||
.HasForeignKey(f => f.PaymentId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(f => f.Order)
|
||||
.WithMany()
|
||||
.IsRequired()
|
||||
.HasForeignKey(f => f.OrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasOne(f => f.Customer)
|
||||
.WithMany()
|
||||
.IsRequired()
|
||||
.HasForeignKey(f => f.CustomerId);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
[EntityTypeConfiguration<RefundConfiguration, Refund>]
|
||||
public class Refund : Models.Refund
|
||||
{
|
||||
public virtual Order? Order { get; set; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
|
||||
public sealed class RefundConfiguration : IEntityTypeConfiguration<Refund>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Refund> builder)
|
||||
{
|
||||
builder.ToTable("Refunds");
|
||||
|
||||
builder.HasKey(r => r.Id);
|
||||
builder.Property(r => r.OrderId).IsRequired();
|
||||
builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||
builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()");
|
||||
builder.Property(o => o.Status).IsRequired();
|
||||
builder.Property(r => r.Amount).IsRequired().HasPrecision(18, 2);
|
||||
builder.Property(r => r.Reason).HasMaxLength(1000);
|
||||
|
||||
builder.HasOne(r => r.Order)
|
||||
.WithMany(o => o.Refunds)
|
||||
.HasForeignKey(r => r.OrderId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
using LiteCharms.Features.Hasher;
|
||||
using LiteCharms.Features.Hasher.Configuration;
|
||||
using LiteCharms.Features.MidrandBooks.Orders;
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Events.Handlers;
|
||||
|
||||
public sealed class PayfastPaymentConfirmationReceivedEventHandler(IServiceProvider services, IOptions<HasherSettings> hasherOptions, ILogger<PayfastPaymentConfirmationReceivedEvent> logger) :
|
||||
INotificationHandler<PayfastPaymentConfirmationReceivedEvent>
|
||||
{
|
||||
private readonly HasherSettings hasherSettings = hasherOptions.Value;
|
||||
|
||||
public async ValueTask Handle(PayfastPaymentConfirmationReceivedEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var hashService = scope.ServiceProvider.GetRequiredService<HashService>();
|
||||
var orderService = scope.ServiceProvider.GetRequiredService<OrderService>();
|
||||
var paymentService = scope.ServiceProvider.GetRequiredService<PaymentService>();
|
||||
var payfastService = scope.ServiceProvider.GetRequiredService<PayfastService>();
|
||||
|
||||
var payload = notification.Payload ?? throw new Exception("Payload metadata context context is null.");
|
||||
|
||||
var dict = payload.ToParamDictionary();
|
||||
var localSignature = PayfastService.GenerateSignature(dict, hasherSettings.PayfastPassphrase);
|
||||
|
||||
if(localSignature.IsFailed)
|
||||
throw new Exception("Failed to generate local signature for incoming webhook payload.");
|
||||
|
||||
if (!string.Equals(localSignature.Value, payload.Signature, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.LogCritical("Incoming webhook signature verification failed. Possible payload tampering.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var hashResult = hashService.DecodeLongIdHash(payload.MerchantPaymentId!);
|
||||
|
||||
if (hashResult.IsFailed) throw new Exception("Failed to decode application tracking hash key identifier.");
|
||||
|
||||
var orderResult = await orderService.GetOrderAsync(hashResult.Value, cancellationToken);
|
||||
|
||||
if (orderResult.IsFailed) throw new Exception("Target system order entity context cannot be traced.");
|
||||
|
||||
var paymentResult = await paymentService.GetOrderPaymentAsync(orderResult.Value.Id, cancellationToken);
|
||||
|
||||
if (paymentResult.IsFailed) throw new Exception("Target payment ledger entity cannot be resolved.");
|
||||
|
||||
decimal.TryParse(payload.AmountGross, CultureInfo.InvariantCulture, out var gross);
|
||||
decimal.TryParse(payload.AmountFee, CultureInfo.InvariantCulture, out var fee);
|
||||
decimal.TryParse(payload.AmountNet, CultureInfo.InvariantCulture, out var net);
|
||||
string status = payload.PaymentStatus ?? "UNKNOWN";
|
||||
|
||||
var isAlreadyProcessed = await paymentService.HasLedgerEntryAsync(orderResult.Value.Id, paymentResult.Value.Id, cancellationToken);
|
||||
|
||||
if (isAlreadyProcessed.Value)
|
||||
{
|
||||
logger.LogWarning("Webhook reference token '{Ref}' already verified. Skipping validation routines.", payload.MerchantPaymentId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (notification.PerformBackgroundChecks)
|
||||
{
|
||||
var isHostValid = await payfastService.ValidateReferrerIpAsync(notification.RemoteIpAddress!, cancellationToken);
|
||||
|
||||
if (isHostValid.IsFailed)
|
||||
throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks.");
|
||||
|
||||
if (!isHostValid.Value)
|
||||
throw new Exception("Security validation exception: Webhook packet source address failed cluster validation checks.");
|
||||
|
||||
var isAmountValid = payfastService.ValidatePaymentAmount(orderResult.Value.Total, payload.AmountGross);
|
||||
|
||||
if (!isAmountValid.Value)
|
||||
throw new Exception("Security validation exception: Transaction cost variance bounds breached.");
|
||||
|
||||
var paramList = new List<string>();
|
||||
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(kvp.Value))
|
||||
{
|
||||
string encoded = HttpUtility.UrlEncode(kvp.Value.Trim());
|
||||
|
||||
string safeValue = PayfastService.PercentEncodingRegex.Replace(encoded, m => m.Value.ToLowerInvariant());
|
||||
paramList.Add($"{kvp.Key}={safeValue}");
|
||||
}
|
||||
}
|
||||
|
||||
string rawParamString = string.Join("&", paramList);
|
||||
|
||||
var serverConfirmation = await payfastService.ValidateServerConfirmationAsync(rawParamString, isSandbox: true, cancellationToken);
|
||||
|
||||
if (serverConfirmation.IsFailed)
|
||||
throw new Exception("Security validation exception: Payfast central handshake server rejected payload legitimacy.");
|
||||
}
|
||||
|
||||
await payfastService.WriteLedgerEntryAsync(new CreateGatewayLedgerEntry
|
||||
{
|
||||
OrderId = orderResult.Value.Id,
|
||||
PaymentId = paymentResult.Value.Id,
|
||||
MerchantPaymentId = payload.MerchantPaymentId!,
|
||||
PayfastPaymentId = payload.PaymentId,
|
||||
CustomerEmail = payload.EmailAddress,
|
||||
AmountFee = fee,
|
||||
AmountGross = gross,
|
||||
AmountNet = net,
|
||||
PaymentStatus = status,
|
||||
}, cancellationToken);
|
||||
|
||||
if (status.Equals("COMPLETE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var ledgerWriteResult = await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry
|
||||
{
|
||||
OrderId = orderResult.Value.Id,
|
||||
PaymentId = paymentResult.Value.Id,
|
||||
PaymentGatewayReference = payload.PaymentId!,
|
||||
Status = LedgerStatuses.Completed,
|
||||
CustomerId = orderResult.Value.CustomerId,
|
||||
}, cancellationToken);
|
||||
|
||||
if (ledgerWriteResult.IsFailed)
|
||||
throw new Exception("Failed to write ledger entry for payment confirmation.");
|
||||
|
||||
var completePaymentResult = await paymentService.CompletePaymentAsync(paymentResult.Value.Id, PaymentStatuses.Paid, cancellationToken);
|
||||
|
||||
if (completePaymentResult.IsFailed)
|
||||
throw new Exception("Failed to update payment status to 'Paid' for payment confirmation.");
|
||||
|
||||
var updateOrderResult = await orderService.UpdateOrderStatusAsync(orderResult.Value.Id, OrderStatus.Completed, cancellationToken);
|
||||
|
||||
if (updateOrderResult.IsFailed)
|
||||
throw new Exception("Failed to update order status to 'Completed' for payment confirmation.");
|
||||
|
||||
logger.LogInformation("Order payment verified secure and cleared successfully.");
|
||||
}
|
||||
else
|
||||
{
|
||||
LedgerStatuses ledgerStatus;
|
||||
|
||||
if (status.Equals("CANCELLED", StringComparison.OrdinalIgnoreCase))
|
||||
ledgerStatus = LedgerStatuses.Cancelled;
|
||||
else
|
||||
ledgerStatus = LedgerStatuses.Failed;
|
||||
|
||||
var ledgerWriteResult = await paymentService.WriteLedgerEntryAsync(new CreateLedgerEntry
|
||||
{
|
||||
OrderId = orderResult.Value.Id,
|
||||
PaymentId = paymentResult.Value.Id,
|
||||
PaymentGatewayReference = payload.PaymentId!,
|
||||
Status = ledgerStatus,
|
||||
CustomerId = orderResult.Value.CustomerId,
|
||||
}, cancellationToken);
|
||||
|
||||
logger.LogInformation("Webhook validation pipeline passed checks successfully, logged entry to ledger with status: {Status}", status);
|
||||
}
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Events;
|
||||
|
||||
public sealed class PayfastPaymentConfirmationReceivedEvent : EventBase, IEvent
|
||||
{
|
||||
public string Name { get; set; } = nameof(PayfastPaymentConfirmationReceivedEvent);
|
||||
|
||||
public PayfastWebhookPayload? Payload { get; set; }
|
||||
|
||||
public string? RemoteIpAddress { get; set; }
|
||||
|
||||
public bool PerformBackgroundChecks { get; set; }
|
||||
|
||||
public PayfastPaymentConfirmationReceivedEvent() { }
|
||||
|
||||
private PayfastPaymentConfirmationReceivedEvent(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true)
|
||||
{
|
||||
Payload = payload;
|
||||
CorrelationId = paymentId;
|
||||
PerformBackgroundChecks = performBackgroundChecks;
|
||||
}
|
||||
|
||||
public static PayfastPaymentConfirmationReceivedEvent Create(PayfastWebhookPayload? payload, string paymentId, bool performBackgroundChecks = true) =>
|
||||
new(payload, paymentId, performBackgroundChecks);
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
public sealed class PayfastWebhookPayload
|
||||
{
|
||||
public string? MerchantId { get; set; }
|
||||
|
||||
public string? MerchantKey { get; set; }
|
||||
|
||||
public string? Signature { get; set; }
|
||||
|
||||
public string? MerchantPaymentId { get; set; }
|
||||
|
||||
public string? PaymentId { get; set; }
|
||||
|
||||
public string? PaymentStatus { get; set; }
|
||||
|
||||
public string? ItemName { get; set; }
|
||||
|
||||
public string? ItemDescription { get; set; }
|
||||
|
||||
public string? AmountGross { get; set; }
|
||||
|
||||
public string? AmountFee { get; set; }
|
||||
|
||||
public string? AmountNet { get; set; }
|
||||
|
||||
public string? NameFirst { get; set; }
|
||||
|
||||
public string? NameLast { get; set; }
|
||||
|
||||
public string? EmailAddress { get; set; }
|
||||
|
||||
public string? CustomStr1 { get; set; }
|
||||
|
||||
public string? CustomInt1 { get; set; }
|
||||
|
||||
public string? Token { get; set; }
|
||||
|
||||
public IDictionary<string, string?> ToParamDictionary() => new Dictionary<string, string?>
|
||||
(StringComparer.Ordinal)
|
||||
{
|
||||
{ "merchant_id", MerchantId },
|
||||
{ "merchant_key", MerchantKey },
|
||||
{ "m_payment_id", MerchantPaymentId },
|
||||
{ "pf_payment_id", PaymentId },
|
||||
{ "payment_status", PaymentStatus },
|
||||
{ "item_name", ItemName },
|
||||
{ "item_description", ItemDescription },
|
||||
{ "amount_gross", AmountGross },
|
||||
{ "amount_fee", AmountFee },
|
||||
{ "amount_net", AmountNet },
|
||||
{ "custom_str1", CustomStr1 },
|
||||
{ "custom_int1", CustomInt1 },
|
||||
{ "name_first", NameFirst },
|
||||
{ "name_last", NameLast },
|
||||
{ "email_address", EmailAddress },
|
||||
{ "token", Token }
|
||||
};
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
public class Payment
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
public long OrderId { get; set; }
|
||||
|
||||
public string? Reference { get; set; }
|
||||
|
||||
public PaymentStatuses Status { get; set; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
public class PaymentGateway
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? Website { get; set; }
|
||||
|
||||
public string? MerchantId { get; set; }
|
||||
|
||||
public string? MerchantKey { get; set; }
|
||||
|
||||
public bool IsSandbox { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
public class PaymentGatewayLedger
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public string? CustomerEmail { get; set; }
|
||||
|
||||
public long OrderId { get; set; }
|
||||
|
||||
public long PaymentId { get; set; }
|
||||
|
||||
public string? MerchantPaymentId { get; set; }
|
||||
|
||||
public string? PayfastPaymentId { get; set; }
|
||||
|
||||
public string? PaymentStatus { get; set; }
|
||||
|
||||
public decimal AmountGross { get; set; }
|
||||
|
||||
public decimal AmountFee { get; set; }
|
||||
|
||||
public decimal AmountNet { get; set; }
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
public class PaymentLedger
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public LedgerStatuses Status { get; set; }
|
||||
|
||||
public long OrderId { get; set; }
|
||||
|
||||
public long PaymentId { get; set; }
|
||||
|
||||
public long CustomerId { get; set; }
|
||||
|
||||
public string? MerchantPaymentId { get; set; }
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
public sealed record CreateGatewayLedgerEntry
|
||||
{
|
||||
public string? CustomerEmail { get; set; }
|
||||
|
||||
public required long OrderId { get; set; }
|
||||
|
||||
public required long PaymentId { get; set; }
|
||||
|
||||
public string? MerchantPaymentId { get; set; }
|
||||
|
||||
public string? PayfastPaymentId { get; set; }
|
||||
|
||||
public string? PaymentStatus { get; set; }
|
||||
|
||||
public decimal AmountGross { get; set; }
|
||||
|
||||
public decimal AmountFee { get; set; }
|
||||
|
||||
public decimal AmountNet { get; set; }
|
||||
}
|
||||
|
||||
public sealed record UpdateRefund
|
||||
{
|
||||
public long OrderId { get; set; }
|
||||
|
||||
public RefundStatus Status { get; set; }
|
||||
|
||||
public string? Reason { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
};
|
||||
|
||||
public sealed record CreateRefund
|
||||
{
|
||||
public long OrderId { get; set; }
|
||||
|
||||
public RefundTypes Type { get; set; }
|
||||
|
||||
public RefundStatus Status { get; set; }
|
||||
|
||||
public string? Reason { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
|
||||
public sealed record CreateLedgerEntry
|
||||
{
|
||||
public required LedgerStatuses Status { get; set; }
|
||||
|
||||
public required long OrderId { get; set; }
|
||||
|
||||
public required long PaymentId { get; set; }
|
||||
|
||||
public required long CustomerId { get; set; }
|
||||
|
||||
public string? PaymentGatewayReference { get; set; }
|
||||
|
||||
public long? PaymentGatewayId { get; set; }
|
||||
}
|
||||
|
||||
public sealed record CreatePaymentGateway
|
||||
{
|
||||
public required string? Name { get; set; }
|
||||
|
||||
public string? Website { get; set; }
|
||||
|
||||
public required string? MerchantId { get; set; }
|
||||
|
||||
public required string? MerchantKey { get; set; }
|
||||
|
||||
public bool IsSandbox { get; set; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
|
||||
public class Refund
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public long OrderId { get; set; }
|
||||
|
||||
public RefundTypes Type { get; set; }
|
||||
|
||||
public RefundStatus Status { get; set; }
|
||||
|
||||
public string? Reason { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.Hasher;
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments;
|
||||
|
||||
public sealed partial class PayfastService(IDbContextFactory<MidrandBooksDbContext> contextFactory,
|
||||
ILogger<PayfastService> logger, IHttpClientFactory httpClientFactory, IConfiguration configuration) : IService
|
||||
{
|
||||
[GeneratedRegex(@"%[0-9A-Fa-f]{2}", RegexOptions.None, matchTimeoutMilliseconds: 1000)]
|
||||
public static partial Regex PercentEncodingRegex { get; }
|
||||
|
||||
public readonly string[] ValidHosts = configuration.GetSection("ValidPayfastHosts").Get<string[]>() ?? [];
|
||||
|
||||
public async ValueTask<Result<long>> WriteLedgerEntryAsync(CreateGatewayLedgerEntry request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if(!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken))
|
||||
return Result.Fail<long>("Referenced order ID does not exist in database.");
|
||||
|
||||
if(!await context.Payments.AnyAsync(p => p.Id == request.PaymentId, cancellationToken))
|
||||
return Result.Fail<long>("Referenced payment ID does not exist in database.");
|
||||
|
||||
var entry = context.GatewayLedger.Add(new Entities.PaymentGatewayLedger
|
||||
{
|
||||
CustomerEmail = request.CustomerEmail,
|
||||
OrderId = request.OrderId,
|
||||
PaymentId = request.PaymentId,
|
||||
MerchantPaymentId = request.MerchantPaymentId,
|
||||
PayfastPaymentId = request.PayfastPaymentId,
|
||||
PaymentStatus = request.PaymentStatus,
|
||||
AmountGross = request.AmountGross,
|
||||
AmountFee = request.AmountFee,
|
||||
AmountNet = request.AmountNet,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(entry.Entity.Id)
|
||||
: Result.Fail<long>("Failed to save Payfast ledger entry to database.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error("Failed to write Payfast ledger entry to database.").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<bool>> ValidateReferrerIpAsync(string remoteIpAddress, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(remoteIpAddress))
|
||||
return Result.Fail<bool>("Remote IP address is null or whitespace.");
|
||||
|
||||
try
|
||||
{
|
||||
var validIps = new HashSet<IPAddress>();
|
||||
|
||||
foreach (var host in ValidHosts)
|
||||
{
|
||||
try
|
||||
{
|
||||
var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
|
||||
|
||||
foreach (var addr in addresses) validIps.Add(addr);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "DNS warning: Failed to resolve Payfast node '{Host}'. It may be decommissioned or unreachable.", host);
|
||||
}
|
||||
}
|
||||
|
||||
if (IPAddress.TryParse(remoteIpAddress, out var incomingIp))
|
||||
{
|
||||
bool isValid = validIps.Contains(incomingIp);
|
||||
|
||||
if (!isValid)
|
||||
logger.LogWarning("SECURITY ALERT: Webhook IP '{RemoteIp}' originated from an unlisted host schema.", remoteIpAddress);
|
||||
|
||||
return Result.Ok(isValid);
|
||||
}
|
||||
|
||||
return Result.Fail<bool>("Invalid remote IP address format.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<bool>(new Error("DNS Verification error while scanning Payfast IP nodes.").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public Result<bool> ValidatePaymentAmount(decimal expectedTotal, string? amountGrossString)
|
||||
{
|
||||
if (!decimal.TryParse(amountGrossString, CultureInfo.InvariantCulture, out decimal grossAmount))
|
||||
return Result.Fail<bool>("Failed to parse payment amount.");
|
||||
|
||||
decimal delta = Math.Abs(expectedTotal - grossAmount);
|
||||
|
||||
bool isAmountValid = delta <= 0.01m;
|
||||
|
||||
if (!isAmountValid)
|
||||
logger.LogError("FINANCIAL DRIFT EXCEPTION: Expected order total R{Expected} but gateway cleared R{Cleared}.", expectedTotal, grossAmount);
|
||||
|
||||
return Result.Ok(isAmountValid);
|
||||
}
|
||||
|
||||
public async ValueTask<Result<bool>> ValidateServerConfirmationAsync(string rawQueryParamString, bool isSandbox, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
string host = isSandbox ? "sandbox.payfast.co.za" : "www.payfast.co.za";
|
||||
string targetUrl = $"https://{host}/eng/query/validate";
|
||||
|
||||
using var content = new StringContent(rawQueryParamString, Encoding.UTF8, "application/x-www-form-urlencoded");
|
||||
|
||||
var httpClient = httpClientFactory.CreateClient();
|
||||
|
||||
var response = await httpClient.PostAsync(targetUrl, content, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode) return Result.Fail<bool>("Failed to validate server confirmation.");
|
||||
|
||||
string responseText = await response.Content.ReadAsStringAsync(ct);
|
||||
|
||||
bool isValidated = string.Equals(responseText.Trim(), "VALID", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!isValidated)
|
||||
logger.LogWarning("SECURITY WARNING: Payfast back-channel returned validation response: '{Response}'", responseText);
|
||||
|
||||
return Result.Ok(isValidated);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<bool>(new Error("Failed to complete back-channel cURL verification handshakes with Payfast remote endpoints.").CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public static Result<string> GenerateSignature(IDictionary<string, string?> data, string? passPhrase = null)
|
||||
{
|
||||
var pfOutput = new StringBuilder();
|
||||
|
||||
foreach (var kvp in data)
|
||||
{
|
||||
if (string.IsNullOrEmpty(kvp.Value))
|
||||
continue;
|
||||
|
||||
string key = kvp.Key;
|
||||
|
||||
string encodedVal = HttpUtility.UrlEncode(kvp.Value.Trim());
|
||||
|
||||
string val = PercentEncodingRegex.Replace(encodedVal, m => m.Value.ToLowerInvariant());
|
||||
|
||||
pfOutput.Append($"{key}={val}&");
|
||||
}
|
||||
|
||||
string getString = pfOutput.Length > 0
|
||||
? pfOutput.ToString()[..^1]
|
||||
: string.Empty;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(passPhrase))
|
||||
{
|
||||
string encodedPassphrase = HttpUtility.UrlEncode(passPhrase.Trim());
|
||||
|
||||
string safePassphrase = PercentEncodingRegex.Replace(encodedPassphrase, m => m.Value.ToLowerInvariant());
|
||||
|
||||
getString += $"&passphrase={safePassphrase}";
|
||||
}
|
||||
|
||||
return HashService.ToMd5Hash(getString);
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
using LiteCharms.Features.Abstractions;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Payments;
|
||||
|
||||
public sealed class PaymentService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||
{
|
||||
public async ValueTask<Result<Payment>> GetOrderPaymentAsync(long orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var payment = await context.Payments.AsNoTracking()
|
||||
.Where(p => p.OrderId == orderId)
|
||||
.OrderByDescending(p => p.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return payment is not null
|
||||
? Result.Ok(payment.ToModel())
|
||||
: Result.Fail<Payment>("Could not find payment for the order");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Payment>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<Refund>> GetRefundAsync(long refundId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var refund = await context.Refunds.AsNoTracking()
|
||||
.FirstOrDefaultAsync(r => r.Id == refundId, cancellationToken);
|
||||
|
||||
return refund is not null
|
||||
? Result.Ok(refund.ToModel())
|
||||
: Result.Fail<Refund>("Could not find refund");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Refund>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdateRefundAsync(long refundId, UpdateRefund request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken))
|
||||
return Result.Fail("Order not found");
|
||||
|
||||
var updatedRows = await context.Refunds
|
||||
.Where(r => r.Id == refundId && r.OrderId == request.OrderId)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(r => r.Status, request.Status)
|
||||
.SetProperty(r => r.Reason, request.Reason)
|
||||
.SetProperty(r => r.UpdatedAt, DateTime.UtcNow)
|
||||
.SetProperty(r => r.Amount, request.Amount), cancellationToken);
|
||||
|
||||
return updatedRows > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Failed to update refund");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreateRefundAsync(CreateRefund request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var order = await context.Orders.AsNoTracking()
|
||||
.FirstOrDefaultAsync(o => o.Id == request.OrderId
|
||||
&& o.Status == OrderStatus.Completed, cancellationToken);
|
||||
|
||||
if (order is null) return Result.Fail("Order not found");
|
||||
|
||||
if (request.Amount > order.Total)
|
||||
return Result.Fail<long>("Refund amount cannot be greater than order total");
|
||||
|
||||
var totalRefundsPaid = await context.Refunds
|
||||
.Where(r => r.OrderId == request.OrderId)
|
||||
.SumAsync(r => r.Amount, cancellationToken);
|
||||
|
||||
if (request.Amount > (order.Total - totalRefundsPaid))
|
||||
return Result.Fail<long>("Refund amount exceeds amount available for refund");
|
||||
|
||||
var refund = context.Refunds.Add(new Entities.Refund
|
||||
{
|
||||
Amount = request.Amount,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
OrderId = request.OrderId,
|
||||
Reason = request.Reason,
|
||||
Status = request.Status,
|
||||
Type = request.Type,
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(refund.Entity.Id)
|
||||
: Result.Fail<long>("Failed to create refund");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<bool>> HasLedgerEntryAsync(long orderId, long paymentId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var exists = await context.Ledger.AnyAsync(l =>
|
||||
l.OrderId == orderId &&
|
||||
l.PaymentId == paymentId, cancellationToken);
|
||||
|
||||
return Result.Ok(exists);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> WriteLedgerEntryAsync(CreateLedgerEntry request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken))
|
||||
return Result.Fail("Order not found");
|
||||
|
||||
if (!await context.Customers.AnyAsync(o => o.Id == request.CustomerId, cancellationToken))
|
||||
return Result.Fail("Customer not found");
|
||||
|
||||
if (!await context.Orders.AnyAsync(oc => oc.Id == request.OrderId && oc.CustomerId == request.CustomerId, cancellationToken))
|
||||
return Result.Fail("Customer does not match the order");
|
||||
|
||||
if (!await context.Payments.AnyAsync(o => o.Id == request.PaymentId && o.OrderId == request.OrderId, cancellationToken))
|
||||
return Result.Fail("Payment not found");
|
||||
|
||||
if (request.PaymentGatewayId is not null)
|
||||
if (!await context.Gateways.AnyAsync(o => o.Id == request.PaymentGatewayId, cancellationToken))
|
||||
return Result.Fail("Gateway not found");
|
||||
|
||||
context.Ledger.Add(new Entities.PaymentLedger
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CustomerId = request.CustomerId,
|
||||
OrderId = request.OrderId,
|
||||
PaymentId = request.PaymentId,
|
||||
Status = request.Status,
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Failed to create ledger entry");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<PaymentGateway>> GetPaymentGatewayAsync(long paymentGatewayId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var gateway = await context.Gateways.AsNoTracking().FirstOrDefaultAsync(g => g.Id == paymentGatewayId, cancellationToken);
|
||||
|
||||
return gateway is not null
|
||||
? Result.Ok(gateway.ToModel())
|
||||
: Result.Fail<PaymentGateway>("Could not find gateway");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<PaymentGateway>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreatePaymentGatewayAsync(CreatePaymentGateway request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (await context.Gateways.AnyAsync(g => g.MerchantId == request.MerchantId && g.MerchantKey == request.MerchantKey, cancellationToken))
|
||||
return Result.Fail<long>("A gateway with the same credentials already exists");
|
||||
|
||||
var gateway = context.Gateways.Add(new Entities.PaymentGateway
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Enabled = true,
|
||||
IsSandbox = request.IsSandbox,
|
||||
MerchantId = request.MerchantId,
|
||||
MerchantKey = request.MerchantKey,
|
||||
Name = request.Name,
|
||||
Website = request.Website,
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(gateway.Entity.Id)
|
||||
: Result.Fail<long>("Failed to create payment gateway");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> CompletePaymentAsync(long paymentId, PaymentStatuses status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (status == PaymentStatuses.NotPaid)
|
||||
return Result.Fail("Cannot finalise a payment using NotPaid status");
|
||||
|
||||
var updatedRecords = await context.Payments
|
||||
.Where(p => p.Id == paymentId && p.Status != PaymentStatuses.Paid && p.Status != status)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(u => u.Status, status)
|
||||
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
|
||||
|
||||
return updatedRecords > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Failed to update payment");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result> UpdatePaymentAsync(long paymentId, decimal amount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
var updatedRecords = await context.Payments
|
||||
.Where(p => p.Id == paymentId && p.Status == PaymentStatuses.NotPaid)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(u => u.Amount, amount)
|
||||
.SetProperty(u => u.Status, PaymentStatuses.NotPaid)
|
||||
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
|
||||
|
||||
return updatedRecords > 0
|
||||
? Result.Ok()
|
||||
: Result.Fail("Failed to update payment");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<Result<long>> CreatePaymentAsync(decimal amount, long orderId, string reference, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||
|
||||
if (await context.Payments.AnyAsync(p => p.OrderId == orderId && p.Amount == amount && p.Status != PaymentStatuses.Paid, cancellationToken))
|
||||
return Result.Fail<long>("An order with the same amount already exists in the system");
|
||||
|
||||
var payment = context.Payments.Add(new Entities.Payment
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
Amount = amount,
|
||||
OrderId = orderId,
|
||||
Reference = reference,
|
||||
Status = PaymentStatuses.NotPaid,
|
||||
});
|
||||
|
||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||
? Result.Ok(payment.Entity.Id)
|
||||
: Result.Fail<long>("Failed to make payment");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Authors.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Categories.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Pages.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Products.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
public sealed class MidrandBooksDbContext(DbContextOptions<MidrandBooksDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Author> Authors => Set<Author>();
|
||||
|
||||
public DbSet<Product> Products => Set<Product>();
|
||||
|
||||
public DbSet<ProductPrice> Prices => Set<ProductPrice>();
|
||||
|
||||
public DbSet<AuthorBook> Books => Set<AuthorBook>();
|
||||
|
||||
public DbSet<BookPage> Pages => Set<BookPage>();
|
||||
|
||||
public DbSet<Contact> Contacts => Set<Contact>();
|
||||
|
||||
public DbSet<Address> Addresses => Set<Address>();
|
||||
|
||||
public DbSet<Customer> Customers => Set<Customer>();
|
||||
|
||||
public DbSet<Order> Orders => Set<Order>();
|
||||
|
||||
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
|
||||
|
||||
public DbSet<Refund> Refunds => Set<Refund>();
|
||||
|
||||
public DbSet<Shipping> Shippings => Set<Shipping>();
|
||||
|
||||
public DbSet<ShippingProvider> ShippingProviders => Set<ShippingProvider>();
|
||||
|
||||
public DbSet<Category> Categories => Set<Category>();
|
||||
|
||||
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
|
||||
|
||||
public DbSet<ProductInventory> Inventories => Set<ProductInventory>();
|
||||
|
||||
public DbSet<Payment> Payments => Set<Payment>();
|
||||
|
||||
public DbSet<PaymentGateway> Gateways => Set<PaymentGateway>();
|
||||
|
||||
public DbSet<PaymentLedger> Ledger => Set<PaymentLedger>();
|
||||
|
||||
public DbSet<PaymentGatewayLedger> GatewayLedger => Set<PaymentGatewayLedger>();
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using static LiteCharms.Features.MidrandBooks.Extensions.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
public sealed class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory<MidrandBooksDbContext>
|
||||
{
|
||||
public MidrandBooksDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddUserSecrets(typeof(MidrandBooksDbContext).Assembly)
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<MidrandBooksDbContext>();
|
||||
optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName));
|
||||
|
||||
return new MidrandBooksDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
-938
@@ -1,938 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
||||
{
|
||||
[DbContext(typeof(MidrandBooksDbContext))]
|
||||
[Migration("20260529070104_Init")]
|
||||
partial class Init
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AuthorId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long>("ProductId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Ranking")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Biography")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Company")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<int>("PublisherType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailImageUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("VatNumber")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Website")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Authors", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<int>("BuildingType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("City")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("CustomerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PostalCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Street")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerId");
|
||||
|
||||
b.ToTable("Addresses", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("CustomerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerId");
|
||||
|
||||
b.ToTable("Contacts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Company")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("VatNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Website")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Customers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("CustomerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("InvoiceUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("Total")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Orders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AuthorBookId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("OrderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ProductPriceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorBookId");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.HasIndex("ProductPriceId");
|
||||
|
||||
b.ToTable("OrderItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AddressId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("OrderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ShippingProviderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TrackingNumber")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AddressId");
|
||||
|
||||
b.HasIndex("OrderId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ShippingProviderId");
|
||||
|
||||
b.ToTable("Shippings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal?>("Price")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("TrackingUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ShippingProviders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AuthorBookId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.PrimitiveCollection<string[]>("Notes")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorBookId");
|
||||
|
||||
b.ToTable("BookPages", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("OrderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.ToTable("Refunds", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.PrimitiveCollection<string[]>("Categories")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("ThumbnailUrls")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Products", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<decimal>("Discount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<long>("ProductId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("Prices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author")
|
||||
.WithMany("Books")
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Author");
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
|
||||
{
|
||||
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
|
||||
{
|
||||
b1.Property<long>("AuthorId");
|
||||
|
||||
b1.Property<int>("__synthesizedOrdinal")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b1.Property<string>("ImageUrl");
|
||||
|
||||
b1.Property<string>("Name");
|
||||
|
||||
b1.Property<int>("Type");
|
||||
|
||||
b1.Property<string>("Url");
|
||||
|
||||
b1.HasKey("AuthorId", "__synthesizedOrdinal");
|
||||
|
||||
b1.ToTable("Authors");
|
||||
|
||||
b1
|
||||
.ToJson("SocialMedia")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("AuthorId");
|
||||
});
|
||||
|
||||
b.Navigation("SocialMedia");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
|
||||
.WithMany("Addresses")
|
||||
.HasForeignKey("CustomerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Customer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
|
||||
.WithMany("Contacts")
|
||||
.HasForeignKey("CustomerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Customer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
|
||||
{
|
||||
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
|
||||
{
|
||||
b1.Property<long>("CustomerId");
|
||||
|
||||
b1.Property<int>("__synthesizedOrdinal")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b1.Property<string>("ImageUrl");
|
||||
|
||||
b1.Property<string>("Name");
|
||||
|
||||
b1.Property<int>("Type");
|
||||
|
||||
b1.Property<string>("Url");
|
||||
|
||||
b1.HasKey("CustomerId", "__synthesizedOrdinal");
|
||||
|
||||
b1.ToTable("Customers");
|
||||
|
||||
b1
|
||||
.ToJson("SocialMedia")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("CustomerId");
|
||||
});
|
||||
|
||||
b.Navigation("SocialMedia");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorBookId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||
.WithMany("OrderItems")
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductPriceId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AuthorBook");
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("ProductPrice");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address")
|
||||
.WithMany()
|
||||
.HasForeignKey("AddressId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||
.WithOne("Shipping")
|
||||
.HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider")
|
||||
.WithMany("Shippings")
|
||||
.HasForeignKey("ShippingProviderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Address");
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("ShippingProvider");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book")
|
||||
.WithMany("Pages")
|
||||
.HasForeignKey("AuthorBookId")
|
||||
.OnDelete(DeleteBehavior.NoAction)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 =>
|
||||
{
|
||||
b1.Property<long>("BookPageId");
|
||||
|
||||
b1.Property<int>("__synthesizedOrdinal")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b1.Property<string>("Description");
|
||||
|
||||
b1.Property<string>("Tag");
|
||||
|
||||
b1.Property<string>("Url");
|
||||
|
||||
b1.HasKey("BookPageId", "__synthesizedOrdinal");
|
||||
|
||||
b1.ToTable("BookPages");
|
||||
|
||||
b1
|
||||
.ToJson("References")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookPageId");
|
||||
});
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||
.WithMany("Refunds")
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
|
||||
{
|
||||
b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 =>
|
||||
{
|
||||
b1.Property<long>("ProductId");
|
||||
|
||||
b1.Property<string>("CopyrightInfo");
|
||||
|
||||
b1.Property<string>("ManufactureDate");
|
||||
|
||||
b1.Property<string>("Manufacturer");
|
||||
|
||||
b1.Property<string>("SerialNumber");
|
||||
|
||||
b1.HasKey("ProductId");
|
||||
|
||||
b1.ToTable("Products");
|
||||
|
||||
b1
|
||||
.ToJson("Metadata")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ProductId");
|
||||
});
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
|
||||
.WithMany("Prices")
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
|
||||
{
|
||||
b.Navigation("Pages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
|
||||
{
|
||||
b.Navigation("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
|
||||
{
|
||||
b.Navigation("Addresses");
|
||||
|
||||
b.Navigation("Contacts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b =>
|
||||
{
|
||||
b.Navigation("OrderItems");
|
||||
|
||||
b.Navigation("Refunds");
|
||||
|
||||
b.Navigation("Shipping");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b =>
|
||||
{
|
||||
b.Navigation("Shippings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
|
||||
{
|
||||
b.Navigation("Prices");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,471 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Init : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Authors",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
PublisherType = table.Column<int>(type: "integer", nullable: false),
|
||||
Company = table.Column<string>(type: "text", nullable: true),
|
||||
VatNumber = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||
Name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
LastName = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
Biography = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
Email = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
|
||||
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),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
|
||||
SocialMedia = table.Column<string>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Authors", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Customers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
Company = table.Column<string>(type: "text", nullable: true),
|
||||
VatNumber = table.Column<string>(type: "text", nullable: true),
|
||||
Email = table.Column<string>(type: "text", nullable: false),
|
||||
Website = table.Column<string>(type: "text", nullable: false),
|
||||
Phone = table.Column<string>(type: "text", nullable: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
|
||||
SocialMedia = table.Column<string>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Customers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Orders",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, 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)", 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)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Orders", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Products",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
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: "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 =>
|
||||
{
|
||||
table.PrimaryKey("PK_Products", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ShippingProviders",
|
||||
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),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
Price = table.Column<decimal>(type: "numeric", nullable: true),
|
||||
TrackingUrl = table.Column<string>(type: "text", nullable: true),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ShippingProviders", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Addresses",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CustomerId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
BuildingType = table.Column<int>(type: "integer", nullable: false),
|
||||
Street = table.Column<string>(type: "text", nullable: false),
|
||||
City = table.Column<string>(type: "text", nullable: false),
|
||||
State = table.Column<string>(type: "text", nullable: false),
|
||||
PostalCode = table.Column<string>(type: "text", nullable: false),
|
||||
Country = table.Column<string>(type: "text", nullable: false),
|
||||
IsPrimary = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Addresses", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Addresses_Customers_CustomerId",
|
||||
column: x => x.CustomerId,
|
||||
principalTable: "Customers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Contacts",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CustomerId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
LastName = table.Column<string>(type: "text", nullable: false),
|
||||
Email = table.Column<string>(type: "text", nullable: false),
|
||||
Phone = table.Column<string>(type: "text", nullable: false),
|
||||
IsPrimary = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Contacts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Contacts_Customers_CustomerId",
|
||||
column: x => x.CustomerId,
|
||||
principalTable: "Customers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Refunds",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
OrderId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<int>(type: "integer", nullable: false),
|
||||
Reason = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
|
||||
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Refunds", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Refunds_Orders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "Orders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Books",
|
||||
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),
|
||||
AuthorId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ProductId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Rating = table.Column<int>(type: "integer", nullable: false),
|
||||
Ranking = table.Column<int>(type: "integer", nullable: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Books", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Books_Authors_AuthorId",
|
||||
column: x => x.AuthorId,
|
||||
principalTable: "Authors",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_Books_Products_ProductId",
|
||||
column: x => x.ProductId,
|
||||
principalTable: "Products",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Prices",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
ProductId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
Discount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Prices", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Prices_Products_ProductId",
|
||||
column: x => x.ProductId,
|
||||
principalTable: "Products",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Shippings",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
OrderId = table.Column<long>(type: "bigint", nullable: false),
|
||||
AddressId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ShippingProviderId = table.Column<long>(type: "bigint", nullable: false),
|
||||
TrackingNumber = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: true),
|
||||
Status = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Shippings", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Shippings_Addresses_AddressId",
|
||||
column: x => x.AddressId,
|
||||
principalTable: "Addresses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Shippings_Orders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "Orders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Shippings_ShippingProviders_ShippingProviderId",
|
||||
column: x => x.ShippingProviderId,
|
||||
principalTable: "ShippingProviders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookPages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
AuthorBookId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
|
||||
Type = table.Column<int>(type: "integer", nullable: false),
|
||||
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: "text[]", nullable: true),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
|
||||
References = table.Column<string>(type: "jsonb", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_BookPages", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookPages_Books_AuthorBookId",
|
||||
column: x => x.AuthorBookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "OrderItems",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||
OrderId = table.Column<long>(type: "bigint", nullable: false),
|
||||
AuthorBookId = table.Column<long>(type: "bigint", nullable: false),
|
||||
ProductPriceId = table.Column<long>(type: "bigint", nullable: false),
|
||||
Quantity = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_OrderItems", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_OrderItems_Books_AuthorBookId",
|
||||
column: x => x.AuthorBookId,
|
||||
principalTable: "Books",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_OrderItems_Orders_OrderId",
|
||||
column: x => x.OrderId,
|
||||
principalTable: "Orders",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_OrderItems_Prices_ProductPriceId",
|
||||
column: x => x.ProductPriceId,
|
||||
principalTable: "Prices",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Addresses_CustomerId",
|
||||
table: "Addresses",
|
||||
column: "CustomerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookPages_AuthorBookId",
|
||||
table: "BookPages",
|
||||
column: "AuthorBookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_AuthorId",
|
||||
table: "Books",
|
||||
column: "AuthorId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Books_ProductId",
|
||||
table: "Books",
|
||||
column: "ProductId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Contacts_CustomerId",
|
||||
table: "Contacts",
|
||||
column: "CustomerId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OrderItems_AuthorBookId",
|
||||
table: "OrderItems",
|
||||
column: "AuthorBookId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OrderItems_OrderId",
|
||||
table: "OrderItems",
|
||||
column: "OrderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_OrderItems_ProductPriceId",
|
||||
table: "OrderItems",
|
||||
column: "ProductPriceId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Prices_ProductId",
|
||||
table: "Prices",
|
||||
column: "ProductId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Refunds_OrderId",
|
||||
table: "Refunds",
|
||||
column: "OrderId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Shippings_AddressId",
|
||||
table: "Shippings",
|
||||
column: "AddressId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Shippings_OrderId",
|
||||
table: "Shippings",
|
||||
column: "OrderId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Shippings_ShippingProviderId",
|
||||
table: "Shippings",
|
||||
column: "ShippingProviderId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookPages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Contacts");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "OrderItems");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Refunds");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Shippings");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Books");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Prices");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Addresses");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Orders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "ShippingProviders");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Authors");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Products");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Customers");
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
-966
@@ -1,966 +0,0 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
||||
{
|
||||
[DbContext(typeof(MidrandBooksDbContext))]
|
||||
[Migration("20260530104851_AddedCategories")]
|
||||
partial class AddedCategories
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AuthorId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<long>("ProductId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Ranking")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Rating")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorId");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Biography")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Company")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<int>("PublisherType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailImageUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("VatNumber")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Website")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Authors", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Categories.Entities.Category", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsMain")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(15)
|
||||
.HasColumnType("character varying(15)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Categories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<int>("BuildingType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("City")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Country")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("CustomerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PostalCode")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Street")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerId");
|
||||
|
||||
b.ToTable("Addresses", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("CustomerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerId");
|
||||
|
||||
b.ToTable("Contacts", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Company")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("VatNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Website")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Customers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("CustomerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("InvoiceUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<decimal>("Total")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Orders", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AuthorBookId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("OrderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ProductPriceId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Quantity")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorBookId");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.HasIndex("ProductPriceId");
|
||||
|
||||
b.ToTable("OrderItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AddressId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("OrderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("ShippingProviderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("TrackingNumber")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AddressId");
|
||||
|
||||
b.HasIndex("OrderId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("ShippingProviderId");
|
||||
|
||||
b.ToTable("Shippings", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<decimal?>("Price")
|
||||
.HasColumnType("numeric");
|
||||
|
||||
b.Property<string>("TrackingUrl")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ShippingProviders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("AuthorBookId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<int>("ContentType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.PrimitiveCollection<string[]>("Notes")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Number")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AuthorBookId");
|
||||
|
||||
b.ToTable("BookPages", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<long>("OrderId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("character varying(1000)");
|
||||
|
||||
b.Property<int>("Status")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("OrderId");
|
||||
|
||||
b.ToTable("Refunds", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.PrimitiveCollection<string[]>("Categories")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("ImageUrl")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("Summary")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("character varying(512)");
|
||||
|
||||
b.PrimitiveCollection<string[]>("ThumbnailUrls")
|
||||
.HasColumnType("text[]");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Products", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("Amount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.Property<decimal>("Discount")
|
||||
.HasPrecision(18, 2)
|
||||
.HasColumnType("numeric(18,2)");
|
||||
|
||||
b.Property<bool>("Enabled")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<long>("ProductId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasDefaultValueSql("now()");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ProductId");
|
||||
|
||||
b.ToTable("Prices", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author")
|
||||
.WithMany("Books")
|
||||
.HasForeignKey("AuthorId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Author");
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
|
||||
{
|
||||
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
|
||||
{
|
||||
b1.Property<long>("AuthorId");
|
||||
|
||||
b1.Property<int>("__synthesizedOrdinal")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b1.Property<string>("ImageUrl");
|
||||
|
||||
b1.Property<string>("Name");
|
||||
|
||||
b1.Property<int>("Type");
|
||||
|
||||
b1.Property<string>("Url");
|
||||
|
||||
b1.HasKey("AuthorId", "__synthesizedOrdinal");
|
||||
|
||||
b1.ToTable("Authors");
|
||||
|
||||
b1
|
||||
.ToJson("SocialMedia")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("AuthorId");
|
||||
});
|
||||
|
||||
b.Navigation("SocialMedia");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
|
||||
.WithMany("Addresses")
|
||||
.HasForeignKey("CustomerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Customer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Contact", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
|
||||
.WithMany("Contacts")
|
||||
.HasForeignKey("CustomerId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Customer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
|
||||
{
|
||||
b.OwnsMany("LiteCharms.Features.Models.SocialMedia", "SocialMedia", b1 =>
|
||||
{
|
||||
b1.Property<long>("CustomerId");
|
||||
|
||||
b1.Property<int>("__synthesizedOrdinal")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b1.Property<string>("ImageUrl");
|
||||
|
||||
b1.Property<string>("Name");
|
||||
|
||||
b1.Property<int>("Type");
|
||||
|
||||
b1.Property<string>("Url");
|
||||
|
||||
b1.HasKey("CustomerId", "__synthesizedOrdinal");
|
||||
|
||||
b1.ToTable("Customers");
|
||||
|
||||
b1
|
||||
.ToJson("SocialMedia")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("CustomerId");
|
||||
});
|
||||
|
||||
b.Navigation("SocialMedia");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook")
|
||||
.WithMany()
|
||||
.HasForeignKey("AuthorBookId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||
.WithMany("OrderItems")
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "ProductPrice")
|
||||
.WithMany()
|
||||
.HasForeignKey("ProductPriceId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("AuthorBook");
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("ProductPrice");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", "Address")
|
||||
.WithMany()
|
||||
.HasForeignKey("AddressId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||
.WithOne("Shipping")
|
||||
.HasForeignKey("LiteCharms.Features.MidrandBooks.Orders.Entities.Shipping", "OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", "ShippingProvider")
|
||||
.WithMany("Shippings")
|
||||
.HasForeignKey("ShippingProviderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Address");
|
||||
|
||||
b.Navigation("Order");
|
||||
|
||||
b.Navigation("ShippingProvider");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Pages.Entities.BookPage", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "Book")
|
||||
.WithMany("Pages")
|
||||
.HasForeignKey("AuthorBookId")
|
||||
.OnDelete(DeleteBehavior.NoAction)
|
||||
.IsRequired();
|
||||
|
||||
b.OwnsMany("LiteCharms.Features.Models.PageReference", "References", b1 =>
|
||||
{
|
||||
b1.Property<long>("BookPageId");
|
||||
|
||||
b1.Property<int>("__synthesizedOrdinal")
|
||||
.ValueGeneratedOnAdd();
|
||||
|
||||
b1.Property<string>("Description");
|
||||
|
||||
b1.Property<string>("Tag");
|
||||
|
||||
b1.Property<string>("Url");
|
||||
|
||||
b1.HasKey("BookPageId", "__synthesizedOrdinal");
|
||||
|
||||
b1.ToTable("BookPages");
|
||||
|
||||
b1
|
||||
.ToJson("References")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("BookPageId");
|
||||
});
|
||||
|
||||
b.Navigation("Book");
|
||||
|
||||
b.Navigation("References");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||
.WithMany("Refunds")
|
||||
.HasForeignKey("OrderId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Order");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
|
||||
{
|
||||
b.OwnsOne("LiteCharms.Features.Models.ProductMetadata", "Metadata", b1 =>
|
||||
{
|
||||
b1.Property<long>("ProductId");
|
||||
|
||||
b1.Property<string>("CopyrightInfo");
|
||||
|
||||
b1.Property<string>("ManufactureDate");
|
||||
|
||||
b1.Property<string>("Manufacturer");
|
||||
|
||||
b1.Property<string>("SerialNumber");
|
||||
|
||||
b1.HasKey("ProductId");
|
||||
|
||||
b1.ToTable("Products");
|
||||
|
||||
b1
|
||||
.ToJson("Metadata")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ProductId");
|
||||
});
|
||||
|
||||
b.Navigation("Metadata");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
|
||||
{
|
||||
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
|
||||
.WithMany("Prices")
|
||||
.HasForeignKey("ProductId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Product");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
|
||||
{
|
||||
b.Navigation("Pages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", b =>
|
||||
{
|
||||
b.Navigation("Books");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", b =>
|
||||
{
|
||||
b.Navigation("Addresses");
|
||||
|
||||
b.Navigation("Contacts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", b =>
|
||||
{
|
||||
b.Navigation("OrderItems");
|
||||
|
||||
b.Navigation("Refunds");
|
||||
|
||||
b.Navigation("Shipping");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.ShippingProvider", b =>
|
||||
{
|
||||
b.Navigation("Shippings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
|
||||
{
|
||||
b.Navigation("Prices");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedCategories : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Categories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "character varying(15)", maxLength: 15, nullable: false),
|
||||
IsMain = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Categories", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Categories");
|
||||
}
|
||||
}
|
||||
}
|
||||
-1007
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user