Compare commits

...

35 Commits

Author SHA1 Message Date
khwezi 92c00360a7 Merge commit '38e765203d6a6e435e4e80368ee3dbb6c4098d2a' 2026-05-29 07:03:28 +00:00
Khwezi Mngoma 38e765203d Refactored migrations
continuous-integration/drone/pr Build is passing
2026-05-29 09:02:47 +02:00
khwezi d9e7f225ae Merge pull request 'midrandshop' (#43) from midrandshop into master
Reviewed-on: #43
2026-05-29 08:23:15 +02:00
Khwezi Mngoma 9eb3526a2e Moved Book Page DTOs
continuous-integration/drone/pr Build is passing
2026-05-29 08:21:11 +02:00
Khwezi Mngoma 4397976ed8 Implemented order service tests 2026-05-29 08:18:29 +02:00
Khwezi Mngoma 2546c34ffc Wrote tests for most services, applied EF core optimisations 2026-05-29 01:05:22 +02:00
Khwezi Mngoma 4e53ff8a37 ProductService tested and stable 2026-05-28 17:28:33 +02:00
Khwezi Mngoma 2a0b34c730 Refactored database registration to allow postgres to use internal representations for afster performance 2026-05-28 09:05:49 +02:00
khwezi 6ae63e2ad1 Merge pull request 'midrandshop' (#42) from midrandshop into master
Reviewed-on: #42
2026-05-27 09:15:44 +02:00
Khwezi Mngoma 902942eee6 Completed initial database design
continuous-integration/drone/pr Build is passing
Sealed qualifying public classes
Migrated database changes
2026-05-27 09:12:04 +02:00
Khwezi Mngoma 70860efcfb Created Order, Refund, Shipping 2026-05-26 08:24:38 +02:00
Khwezi Mngoma 20b747e89c Added Order models 2026-05-26 00:47:07 +02:00
Khwezi Mngoma 7136e4fc70 Added Customer, Contact and Address with Service
Labeled all service to enable assembly scanning
2026-05-26 00:27:11 +02:00
Khwezi Mngoma 4a85d01d1a Included navigation fields in get queries 2026-05-25 23:00:17 +02:00
Khwezi Mngoma d55bf4f82f Created Author, Book, AuthorBook, Page and Product with Price 2026-05-25 22:18:53 +02:00
khwezi f5efdde37c Merge pull request 'Refactored connection string name' (#41) from midrandshop into master
Reviewed-on: #41
2026-05-24 13:51:11 +02:00
Khwezi Mngoma 87da491ed6 Refactored connection string name
continuous-integration/drone/pr Build is passing
2026-05-24 13:50:41 +02:00
khwezi 1592d5dc8f Merge pull request 'midrandshop' (#40) from midrandshop into master
Reviewed-on: #40
2026-05-24 13:43:22 +02:00
Khwezi Mngoma 08a64d1578 Added test MidrandBooks projet
continuous-integration/drone/pr Build is passing
2026-05-24 13:42:59 +02:00
Khwezi Mngoma f67f5eaf53 Added MidrandBooks project 2026-05-24 13:38:47 +02:00
khwezi 50a8a59d92 Merge pull request 'Split Features to create space for more projects' (#39) from midrandshop into master
Reviewed-on: #39
2026-05-24 13:19:55 +02:00
Khwezi Mngoma 70c6e0bfbc Split Features to create space for more projects
continuous-integration/drone/pr Build is passing
2026-05-24 13:19:09 +02:00
khwezi b70d9559b0 Merge pull request 'midrandshop' (#38) from midrandshop into master
Reviewed-on: #38
2026-05-23 11:55:02 +02:00
Khwezi Mngoma 032b9e1818 Updated nuget packages
continuous-integration/drone/pr Build is passing
2026-05-23 11:54:32 +02:00
Khwezi Mngoma 424c1c6f8c Fixed email test 2026-05-23 11:52:47 +02:00
khwezi 81d5e8f07c Merge pull request 'Added MidrandShop feature and spl;it extensions and healthchecks' (#37) from midrandshop into master
Reviewed-on: #37
2026-05-23 11:49:27 +02:00
Khwezi Mngoma 7d5e9a18d8 Added MidrandShop feature and spl;it extensions and healthchecks
continuous-integration/drone/pr Build is failing
2026-05-23 11:48:47 +02:00
khwezi 20a53942b5 Merge pull request 'Added product metadata' (#36) from s3service into master
Reviewed-on: #36
2026-05-20 21:15:01 +02:00
Khwezi Mngoma 3656223b5f Added product metadata
continuous-integration/drone/pr Build is passing
Added dates to product model
Migrated database changes
2026-05-20 21:13:58 +02:00
khwezi 9edb2aa4aa Merge pull request 'Optimised UploadFileAsync()' (#35) from s3service into master
Reviewed-on: #35
2026-05-20 15:33:47 +02:00
Khwezi Mngoma ccf30ac36b Optimised UploadFileAsync()
continuous-integration/drone/pr Build is passing
Implemented and tested DeleteFileAsync()
2026-05-20 15:32:54 +02:00
khwezi 6ed023f2cf Merge pull request 'Refactored the S3 services to properly upload the file' (#34) from s3service into master
Reviewed-on: #34
2026-05-20 08:03:43 +02:00
Khwezi Mngoma d6fdf1b9c8 Refactored the S3 services to properly upload the file
continuous-integration/drone/pr Build is passing
2026-05-20 08:01:44 +02:00
khwezi 2c9f5a846c Merge pull request 'Updated how i use configs' (#33) from s3service into master
Reviewed-on: #33
2026-05-19 14:57:59 +02:00
Khwezi Mngoma 89a343a85f Updated how i use configs
continuous-integration/drone/pr Build is passing
2026-05-19 14:57:14 +02:00
214 changed files with 8506 additions and 593 deletions
+5 -1
View File
@@ -19,6 +19,10 @@ 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
@@ -41,7 +45,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\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* LiteCharms.Features.TechShop\n* LiteCharms.Features.MidrandBooks\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
\"draft\": false,
\"prerelease\": false
}"
+39
View File
@@ -3,6 +3,45 @@ 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 ####
@@ -0,0 +1,81 @@
using LiteCharms.Features.MidrandBooks.Authors;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
[IntegrationFact]
public async Task CreateAuthorAsync_ShouldReturn_ResultWithAuthorId()
{
var request = new CreateAuthor
{
Name = "John",
LastName = "Doe",
Company = "Solo Publishers",
Email = "solo@publishers.co.za",
PublisherType = PublisherTypes.Independent,
ImageUrl = ""
};
var result = await authorService.CreateAuthorAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateAuthorAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateAuthor
{
Name = "Jane",
LastName = "Doe",
Company = "Solo Publishers",
Email = "solo@publishers.co.za",
PublisherType = PublisherTypes.Independent,
ImageUrl = ""
};
var result = await authorService.UpdateAuthorAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetAuthors_ShouldReturn_ResultWithAuthorList()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await authorService.GetAuthorsAsync(range, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetAuthorAsync_ShouldReturn_ResultWithAuthor()
{
var result = await authorService.GetAuthorAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task UpdateAuthorStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await authorService.UpdateAuthorStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -0,0 +1,52 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class BooksServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly BooksService bookService = fixture.Services.GetRequiredService<BooksService>();
[IntegrationFact]
public async Task CreateBookAsync_ShouldReturn_ResultWithBookId()
{
var result = await bookService.CreateBookAsync(1, 2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetBookAsync_ShouldReturn_ResultWithBook()
{
var result = await bookService.GetBookAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetBooksByAuthorAsync_ShouldReturn_ResultWithAuthorBooks()
{
var result = await bookService.GetBooksByAuthorAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetPublishedBooksAsync_ShouldReturn_ResultWithBublishedBooks()
{
var result = await bookService.GetPublishedBooksAsync(0, 1000, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task UpdateBookStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await bookService.UpdateBookStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -0,0 +1,43 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.MidrandBooks.Abstractions;
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()
.AddMediator()
.AddLogging()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddMidrandShopDatabase(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.AddShopServices()
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
}
public void Dispose() { }
}
@@ -0,0 +1,10 @@
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
public class IntegrationFactAttribute : FactAttribute
{
public IntegrationFactAttribute()
{
if(!Debugger.IsAttached)
Skip = "This test requires the debugger to be attached.";
}
}
@@ -0,0 +1,201 @@
using LiteCharms.Features.MidrandBooks.Customers;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class CustomerServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly CustomerService customerService = fixture.Services.GetRequiredService<CustomerService>();
[IntegrationFact]
public async Task CreateCustomerAsync_ShouldReturn_ResultWithCustomerId()
{
var request = new CreateCustomer
{
Company = "Book Lovers",
Email = "hank@booklovers.com",
Phone = "555 1245 8577",
Website = "https://www.booklovers.com"
};
var result = await customerService.CreateCustomerAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task CreateCustomerContactAsync_ShouldReturn_ResultWithCustomerContactId()
{
var request = new CreateCustomerContact
{
Name = "Sipho",
LastName = "Madlanga",
Phone = "0710857365",
Email = "sipho@madlanga.africa",
Type = ContactTypes.Business
};
var result = await customerService.CreateCustomerContactAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task CreateCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddressId()
{
var request = new CreateCustomerAddress
{
Name = "Business",
BuildingType = AddressBuildingTypes.MixedUse,
Type = AddressType.Shipping,
Street = "123 Building 4, XYZ Suburb, Some Region",
City = "Johannesburg",
State = "Gauteng",
Country = "South Africa",
IsPrimary = true,
Enabled = true,
PostalCode = "12345"
};
var result = await customerService.CreateCustomerAddressAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateCustomerAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomer
{
Company = "Book Lovers",
Email = "hank@booklovers.com",
Phone = "555 1245 8578",
Website = "https://www.booklovers.com"
};
var result = await customerService.UpdateCustomerAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerContactAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomerContact
{
Name = "Sipho",
LastName = "Madlanga",
Phone = "0710857366",
Email = "sipho@madlanga.africa",
Type = ContactTypes.Business
};
var result = await customerService.UpdateCustomerContactAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerAddressAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomerAddress
{
Name = "Business",
BuildingType = AddressBuildingTypes.MixedUse,
Type = AddressType.Shipping,
Street = "123 Building 4, XYZ Suburb, Some Region",
City = "Johannesburg",
State = "Gauteng",
Country = "South Africa",
IsPrimary = true,
Enabled = true,
PostalCode = "12346"
};
var result = await customerService.UpdateCustomerAddressAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerContactStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerContactStatusAsync(1, true, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerAddressStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerAddressStatusAsync(1, true, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetCustomersAsync_ShouldReturn_ResultWithCustomerList()
{
var result = await customerService.GetCustomersAsync(fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerContactsAsync_ShouldReturn_ResultWithCustomerContactList()
{
var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAddressesAsync_ShouldReturn_ResultWithCustomerAddressList()
{
var result = await customerService.GetCustomerAddressesAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAsync_ShouldReturn_ResultWithCustomer()
{
var result = await customerService.GetCustomerAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetCustomerContactAsync_ShouldReturn_ResultWithCustomerContact()
{
var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddress()
{
var result = await customerService.GetCustomerAddressAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
}
@@ -0,0 +1,53 @@
<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.5.1" />
<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.Text.Json" />
<Using Include="System.Diagnostics" />
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,196 @@
using LiteCharms.Features.MidrandBooks.Orders;
using LiteCharms.Features.MidrandBooks.Orders.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class OrderServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly OrderService orderService = fixture.Services.GetRequiredService<OrderService>();
[IntegrationFact]
public async Task CreateOrderAsync_ShouldReturn_ResultWithOrderId()
{
var request = new CreateOrder(250, "At the intercomm, dial 1 then option 2");
var result = await orderService.CreateOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task AddItemToOrderAsync_ShouldReturn_ResultWithOrderItemId()
{
var request = new CreateOrderItem(1, 1, 2);
var result = await orderService.AddItemToOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task AddItemsToOrderAsync_ShouldReturn_ResultWithSuccess()
{
var requests = new List<CreateOrderItem>
{
new(1, 1, 1),
new(1, 1, 3)
};
var result = await orderService.AddItemsToOrderAsync(1, [.. requests], fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task RemoveItemFromOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.RemoveItemFromOrderAsync(1, 5, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task ClearOrderItemsAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.ClearOrderItemsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CancelOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.CancelOrderAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetOrderAsync_ShouldReturn_ResultWithOrder()
{
var result = await orderService.GetOrderAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetOrdersByCustomerAsync_ShouldReturn_ResultWithOrderList()
{
var result = await orderService.GetOrdersByCustomerAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetOrdersAsync_ShouldReturn_ResultWithOrderList()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await orderService.GetOrdersAsync(range, 0, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task UpdateOrderStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateOrderStatusAsync(1, OrderStatus.Pending, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task AddShippingToOrderAsync_ShouldReturn_ResultWithSuccess()
{
var request = new CreateShipping(1, 2);
var result = await orderService.AddShippingToOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateShippingStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateShippingStatusAsync(1, ShippingStatuses.Shipped, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetShippingByOrderIdAsync_ShouldReturn_ResultWithShipping()
{
var result = await orderService.GetShippingByOrderIdAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task RemoveShippingFromOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.RemoveShippingFromOrderAsync(1, 1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateShippingTrackingNumberAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateShippingTrackingNumberAsync(1, 2, "NA0009969397");
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithShippingProviderId()
{
var request = new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel");
var result = await orderService.CreateShippingProviderAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task GetShippingProvidersAsync_ShouldReturn_ResultWithShippingProviderList()
{
var result = await orderService.GetShippingProvidersAsync(true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetShippingProviderAsync_ShouldReturn_ResultWithShippingProvider()
{
var result = await orderService.GetShippingProviderAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task UpdateShippingProviderAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateShippingProvider(2,true, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel");
var result = await orderService.UpdateShippingProviderAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -0,0 +1,63 @@
using LiteCharms.Features.MidrandBooks.Pages;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class PageServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly PageService pageService = fixture.Services.GetRequiredService<PageService>();
[IntegrationFact]
public async Task CreatePageAsync_ShouldReturn_ResultWithPageId()
{
}
[IntegrationFact]
public async Task GetPagesAsync_ByBookId_ShouldReturn_ResultWithPageList()
{
}
[IntegrationFact]
public async Task GetPageAsync_ShouldReturn_ResultWithPage()
{
}
[IntegrationFact]
public async Task GetPageByNumberAsync_ById_And_BookPageNumber_ShouldReturn_ResultWithPage()
{
}
[IntegrationFact]
public async Task UpdatePageAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeletePageAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeleteByPageTypeAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeleteAllAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task UpdatePageStatusAsync_ShouldReturn_ResultWithSuccess()
{
}
}
@@ -0,0 +1,143 @@
using LiteCharms.Features.MidrandBooks.Products;
using LiteCharms.Features.MidrandBooks.Products.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly ProductService productService = fixture.Services.GetRequiredService<ProductService>();
[IntegrationFact]
public async Task GetProductPriceAsync_ShouldReturn_RetultOneProductPrice()
{
var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductPricesAsync_ShouldReturn_RetultProductPriceList()
{
var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task SearchProductsAsync_ShouldReturn_RetultMatchingProducts()
{
var filter = new ProductFilter
{
Name = "system",
Manufacturer = "techwave",
SerialNumber = "2024",
MinPrice = 10,
MaxPrice = 30
};
var result = await productService.SearchProductsAsync(filter, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductAsync_ShouldReturn_RetultOneProduct()
{
var result = await productService.GetProductAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductsAsync_ShouldReturn_RetultProducts()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await productService.GetProductsAsync(0, range, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue()
{
var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateProductPriceStatusAsync_ShouldReturn_ResultTrue()
{
var result = await productService.UpdateProductPriceStatusAsync(2, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CreateProductPriceAsync_Should_Return_NewProductPriceId()
{
var request = new CreateProductPrice
{
Amount = 29.99m,
Discount = 0.00m
};
var result = await productService.CreateProductPriceAsync(2, request, fixture.CancellationToken);
Assert.True(result.IsSuccess, "Product price creation should be successful.");
Assert.True(result.Value > 0, "New ProductPriceId should be greater than 0.");
output.WriteLine($"Created ProductPriceId: {result.Value}");
}
[IntegrationFact]
public async Task CreateProductAsync_Result_Returns_ProductId()
{
var request = new CreateProduct
{
Name = "Systems Rewired",
Description = "[Design], <Code>, AND /CHAORS/ IN ***SYNC***",
Summary = "A comprehensive guide to systems thinking and design.",
ImageUrl = "https://bookshop.cdn.khongisa.co.za/design/2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp",
Type = ProductTypes.Book,
Categories = ["Systems Thinking", "Design", "Programming"],
Metadata = new ProductMetadata
{
CopyrightInfo = "© 2024 John Doe. All rights reserved.",
ManufactureDate = "2024-06-01",
Manufacturer = "TechWave Publishing",
SerialNumber = "SR-2024-0001"
}
};
var result = await productService.CreateProductAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess, "Product creation should be successful.");
Assert.True(result.Value > 0, "ProductId should be greater than 0.");
output.WriteLine($"Created ProductId: {result.Value}");
}
}
@@ -0,0 +1,28 @@
{
"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": "*"
}
@@ -0,0 +1,3 @@
namespace LiteCharms.Features.MidrandBooks.Abstractions;
public interface IService;
@@ -0,0 +1,143 @@
using LiteCharms.Features.MidrandBooks.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));
}
}
}
@@ -0,0 +1,14 @@
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; } = [];
}
@@ -0,0 +1,28 @@
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);
}
}
@@ -0,0 +1,24 @@
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; }
}
@@ -0,0 +1,147 @@
using LiteCharms.Features.MidrandBooks.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> 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));
}
}
}
@@ -0,0 +1,9 @@
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; } = [];
}
@@ -0,0 +1,25 @@
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(); });
}
}
@@ -0,0 +1,36 @@
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; }
}
@@ -0,0 +1,30 @@
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; }
}
@@ -0,0 +1,398 @@
using LiteCharms.Features.MidrandBooks.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));
}
}
}
@@ -0,0 +1,7 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
[EntityTypeConfiguration<AddressConfiguration, Address>]
public class Address : Models.Address
{
public virtual Customer? Customer { get; set; }
}
@@ -0,0 +1,29 @@
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);
}
}
@@ -0,0 +1,7 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
[EntityTypeConfiguration<ContactConfiguration, Contact>]
public class Contact : Models.Contact
{
public virtual Customer? Customer { get; set; }
}
@@ -0,0 +1,26 @@
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);
}
}
@@ -0,0 +1,9 @@
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; } = [];
}
@@ -0,0 +1,21 @@
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(); });
}
}
@@ -0,0 +1,32 @@
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; }
}
@@ -0,0 +1,26 @@
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; }
}
@@ -0,0 +1,26 @@
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; }
}
@@ -0,0 +1,60 @@
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;
+62
View File
@@ -0,0 +1,62 @@
namespace LiteCharms.Features.MidrandBooks;
public enum PublisherTypes : int
{
Individual = 0,
Company = 1,
Organization = 2,
SelfPublished = 3,
UniversityPress = 4,
GovernmentAgency = 5,
NonProfit = 6,
Independent = 7
}
public enum BookTypes : int
{
Fiction = 0,
NonFiction = 1,
Academic = 2,
SelfHelp = 3,
Biography = 4,
Poetry = 5,
Children = 6,
YoungAdult = 7,
ScienceFiction = 8,
Fantasy = 9
}
public enum BookContentTypes : int
{
Text = 0,
Image = 1,
Video = 2,
Audio = 3,
Interactive = 4,
Markdown = 5,
Html = 6,
Json = 7,
Yaml = 8
}
public enum BookPageTypes : int
{
Cover = 0,
Preface = 1,
Introduction = 2,
Content = 3,
Closing = 4,
Referencer = 5,
Credits = 6,
BackCover = 7
}
public enum ProductTypes : int
{
Book = 1,
Journal = 2,
Magazine = 3,
EBook = 4,
Audiobook = 5,
Accessory = 6
}
@@ -0,0 +1,30 @@
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;
}
}
@@ -0,0 +1,179 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Orders.Models;
using LiteCharms.Features.MidrandBooks.Pages.Models;
using LiteCharms.Features.MidrandBooks.Products.Models;
namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class Mappers
{
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)
{
return 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,
Categories = entity.Categories,
Enabled = entity.Enabled,
Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null,
};
}
public static Author ToModel(this Authors.Entities.Author entity) => new()
{
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
};
}
@@ -0,0 +1,26 @@
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;
}
}
@@ -0,0 +1,19 @@
using LiteCharms.Features.MidrandBooks.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;
}
}
@@ -0,0 +1,28 @@
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");
}
}
}
@@ -0,0 +1,28 @@
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);
}
}
}
@@ -0,0 +1,166 @@
<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="Meziantou.Analyzer" Version="3.0.96">
<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.1" />
<!-- 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.16.0" />
<PackageReference Include="MimeKit" Version="4.16.0" />
<!-- Global Usings-->
<Using Include="MimeKit" />
<Using Include="MailKit.Net.Smtp" />
</ItemGroup>
<!-- CQRS -->
<ItemGroup>
<PackageReference Include="FluentResults" Version="4.0.0" />
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
<!-- Global Usings -->
<Using Include="FluentResults" />
<Using Include="Mediator" />
</ItemGroup>
<!-- Amazon S3 SDK -->
<ItemGroup>
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.4.1" />
<PackageReference Include="AWSSDK.S3" Version="4.0.23.4" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
<!-- global Usings -->
<Using Include="Amazon.S3" />
<Using Include="Amazon.S3.Model" />
<Using Include="Amazon.Runtime" />
</ItemGroup>
<!-- Shared Usings -->
<ItemGroup>
<Using Include="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>
@@ -0,0 +1,13 @@
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; } = [];
}
@@ -0,0 +1,17 @@
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);
}
}
@@ -0,0 +1,14 @@
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; }
}
@@ -0,0 +1,31 @@
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);
}
}
@@ -0,0 +1,13 @@
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; }
}
@@ -0,0 +1,33 @@
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);
}
}
@@ -0,0 +1,6 @@
namespace LiteCharms.Features.MidrandBooks.Orders.Entities;
public class ShippingProvider : Models.ShippingProvider
{
public virtual ICollection<Shipping> Shippings { get; set; } = [];
}
@@ -0,0 +1,18 @@
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);
}
}
@@ -0,0 +1,20 @@
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; }
}
@@ -0,0 +1,16 @@
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; }
}
@@ -0,0 +1,11 @@
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);
@@ -0,0 +1,20 @@
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; }
}
@@ -0,0 +1,20 @@
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; }
}
@@ -0,0 +1,462 @@
using LiteCharms.Features.MidrandBooks.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));
}
}
}
@@ -0,0 +1,9 @@
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();
}
@@ -0,0 +1,27 @@
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);
}
}
@@ -0,0 +1,28 @@
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; }
}
@@ -0,0 +1,20 @@
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;
@@ -0,0 +1,210 @@
using LiteCharms.Features.MidrandBooks.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));
}
}
}
@@ -0,0 +1,9 @@
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; }
}
@@ -0,0 +1,22 @@
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);
}
}
@@ -0,0 +1,20 @@
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; }
}
@@ -0,0 +1,7 @@
using LiteCharms.Features.MidrandBooks.Abstractions;
namespace LiteCharms.Features.MidrandBooks.Payments;
public sealed class PaymentService : IService
{
}
@@ -0,0 +1,38 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
using LiteCharms.Features.MidrandBooks.Authors.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>();
}
@@ -0,0 +1,20 @@
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);
}
}
@@ -0,0 +1,938 @@
// <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
}
}
}
@@ -0,0 +1,471 @@
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");
}
}
}
@@ -0,0 +1,935 @@
// <auto-generated />
using System;
using LiteCharms.Features.MidrandBooks.Postgres;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{
[DbContext(typeof(MidrandBooksDbContext))]
partial class MidrandBooksDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(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
}
}
}
@@ -0,0 +1,7 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
[EntityTypeConfiguration<ProductConfiguration, Product>]
public class Product : Models.Product
{
public virtual ICollection<ProductPrice> Prices { get; set; } = [];
}
@@ -0,0 +1,25 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.Ignore(p => p.Price);
builder.ToTable("Products");
builder.HasKey(f => f.Id);
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(255);
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
builder.Property(f => f.Description).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).HasMaxLength(1024);
builder.Property(f => f.Enabled).HasDefaultValue(false);
builder.Property(f => f.Categories).IsRequired(false);
builder.Property(f => f.ThumbnailUrls).IsRequired(false);
builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); });
}
}
@@ -0,0 +1,7 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
public class ProductPrice : Models.ProductPrice
{
public virtual Product? Product { get; set; }
}
@@ -0,0 +1,22 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
public sealed class ProductPriceConfiguration : IEntityTypeConfiguration<ProductPrice>
{
public void Configure(EntityTypeBuilder<ProductPrice> builder)
{
builder.ToTable("Prices");
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.ProductId).IsRequired();
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.Discount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.Enabled).HasDefaultValue(false);
builder.HasOne(f => f.Product)
.WithMany(p => p.Prices)
.HasForeignKey(f => f.ProductId)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -0,0 +1,32 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public class Product
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public ProductTypes Type { get; set; }
public string? Name { get; set; }
public string? Summary { get; set; }
public string? Description { get; set; }
public string? ImageUrl { get; set; }
public string[]? ThumbnailUrls { get; set; }
public string[]? Categories { get; set; }
public ProductMetadata? Metadata { get; set; }
public ProductPrice? Price { get; set; }
public bool Enabled { get; set; }
}
@@ -0,0 +1,18 @@
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public class ProductPrice
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public long ProductId { get; set; }
public decimal Amount { get; set; }
public decimal Discount { get; set; }
public bool Enabled { get; set; }
}
@@ -0,0 +1,29 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public sealed record CreateProduct
{
public required ProductTypes Type { get; set; }
public required string Name { get; set; }
public required string Summary { get; set; }
public required string Description { get; set; }
public required string ImageUrl { get; set; }
public string[]? ThumbnailUrls { get; set; }
public string[]? Categories { get; set; }
public ProductMetadata? Metadata { get; set; }
}
public sealed class CreateProductPrice
{
public decimal Amount { get; set; }
public decimal Discount { get; set; }
}
@@ -0,0 +1,257 @@
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.MidrandBooks.Products.Models;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Products;
public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result> UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rowsUpdated = await context.Prices
.Where(p => p.Id == productPriceId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Enabled, isEnabled)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error($"Product price with ID {productPriceId} not found"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateProductStatusAsync(long productId, bool isEnabled, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rowsUpdated = await context.Products
.Where(p => p.Id == productId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Enabled, isEnabled)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
return rowsUpdated > 0
? Result.Ok()
: Result.Fail(new Error($"Product with ID {productId} not found"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Product[]>> SearchProductsAsync(ProductFilter filter, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Products.AsQueryable();
var cultureInfo = CultureInfo.InvariantCulture;
if (!string.IsNullOrWhiteSpace(filter.Name))
query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Name}%"));
if (!string.IsNullOrWhiteSpace(filter.Title))
query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%"));
if (!string.IsNullOrWhiteSpace(filter.Category))
query = query.Where(p => p.Categories.Contains(filter.Category));
if (!string.IsNullOrWhiteSpace(filter.Manufacturer))
query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%"));
if (!string.IsNullOrWhiteSpace(filter.SerialNumber))
query = query.Where(p => EF.Functions.ILike(p.Metadata!.SerialNumber!, $"%{filter.SerialNumber}%"));
if (filter.MinPrice > 0)
query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice));
var products = await query.AsNoTracking().Where(p => p.Enabled).ToListAsync(cancellationToken);
return products?.Count > 0
? Result.Ok(products.Select(p => p.ToModel()).ToArray())
: Result.Fail<Product[]>("No products found.");
}
catch (Exception ex)
{
return Result.Fail<Product[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateProductPriceAsync(long productId, CreateProductPrice request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken))
return Result.Fail<long>($"Product with ID {productId} does not exist.");
var existingPrices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken);
if (existingPrices.Count > 0)
foreach (var existingPrice in existingPrices)
{
existingPrice.Enabled = false;
existingPrice.UpdatedAt = DateTime.UtcNow;
context.Prices.Update(existingPrice);
}
var price = context.Prices.Add(new Entities.ProductPrice
{
ProductId = productId,
Amount = request.Amount,
Discount = request.Discount,
Enabled = true,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(price.Entity.Id)
: Result.Fail<long>("Failed to create product price.");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken))
return Result.Fail<long>("A product with the same name already exists.");
if (request.Metadata is not null)
if (await context.Products.AnyAsync(p => p.Metadata!.SerialNumber == request.Metadata.SerialNumber, cancellationToken))
return Result.Fail<long>("A product with the same metadata already exists.");
var product = context.Products.Add(new Entities.Product
{
UpdatedAt = DateTime.UtcNow,
Type = request.Type,
Name = request.Name,
Summary = request.Summary,
Description = request.Description,
ImageUrl = request.ImageUrl,
ThumbnailUrls = request.ThumbnailUrls,
Metadata = request.Metadata,
Categories = request.Categories,
Enabled = true
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(product.Entity.Id)
: Result.Fail<long>("Failed to create product.");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ProductPrice>> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Prices
.AsNoTracking()
.OrderByDescending(p => p.CreatedAt)
.ThenBy(p => p.UpdatedAt)
.FirstOrDefaultAsync(p => p.ProductId == productId && p.Enabled, cancellationToken);
return product is not null
? Result.Ok(product.ToModel())
: Result.Fail<ProductPrice>(new Error($"No price found for product with ID {productId}"));
}
catch (Exception ex)
{
return Result.Fail<ProductPrice>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ProductPrice[]>> GetProductPricesAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var prices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken);
return prices?.Count > 0
? prices.Select(p => p.ToModel()).ToArray()
: Result.Fail<ProductPrice[]>(new Error($"No prices found for product with ID {productId}"));
}
catch (Exception ex)
{
return Result.Fail<ProductPrice[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Product>> GetProductAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
return product is null
? Result.Fail<Product>(new Error($"Product with ID {productId} not found."))
: Result.Ok(product.ToModel());
}
catch (Exception ex)
{
return Result.Fail<Product>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Product[]>> GetProductsAsync(int offset, 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 products = await context.Products
.AsNoTracking()
.Include(p => p.Prices)
.OrderByDescending(p => p.CreatedAt)
.ThenByDescending(p => p.UpdatedAt)
.Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate)
.Skip(offset)
.Take(range.MaxRecords)
.AsSplitQuery()
.ToArrayAsync(cancellationToken);
return products?.Length > 0
? Result.Ok(products.Select(p => p.ToModel()).ToArray())
: Result.Fail<Product[]>(new Error("Failed to retrieve products."));
}
catch (Exception ex)
{
return Result.Fail<Product[]>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -0,0 +1,38 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.TechShop.Extensions;
namespace LiteCharms.Features.TechShop.Tests;
public class Fixture : IDisposable
{
public IConfiguration Configuration { get; set; }
public IServiceProvider Services { get; set; }
public IMediator Mediator { get; set; }
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()
.AddMediator()
.AddLogging()
.AddTechShopServices()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddTechShopDatabase(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
}
public void Dispose() { }
}
@@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>fa06c1a6-2ba8-4c47-b19b-11e0f78e7b92</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.5.1" />
<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.TechShop\LiteCharms.Features.TechShop.csproj" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -1,27 +1,28 @@
using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Notifications;
using LiteCharms.Features.Shop.Notifications.Events;
using LiteCharms.Features.TechShop.Notifications;
using LiteCharms.Features.TechShop.Notifications.Events;
using static LiteCharms.Features.Extensions.Email;
namespace LiteCharms.Features.Tests;
namespace LiteCharms.Features.TechShop.Tests;
public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture<CommonFixture>
public class NotificationsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly NotificationService notificationService = fixture.Services.GetRequiredService<NotificationService>();
[Fact]
public async Task CreateNotificationCommand_ShouldSucceed()
{
Shop.Notifications.Models.CreateNotification request = new()
Notifications.Models.CreateNotification request = new()
{
CorrelationId = Guid.CreateVersion7().ToString(),
CorrelationIdType = Shop.CorrelationIdTypes.None,
Direction = Shop.NotificationDirection.Outgoing,
Platform = Shop.NotificationPlatforms.Email,
Priority = Shop.Priorities.Medium,
CorrelationIdType = CorrelationIdTypes.None,
Direction = NotificationDirection.Outgoing,
Platform = NotificationPlatforms.Email,
Priority = Priorities.Medium,
Sender = "xUnit Test",
SenderAddress = "khwezi@mngoma.africa",
Recipient = $"{Email.Extensions.Constants.ShopEmailFromName} [Test]",
RecipientAddress = Email.Extensions.Constants.ShopEmailFromAddress,
Recipient = $"{ShopEmailFromName} [Test]",
RecipientAddress = ShopEmailFromAddress,
Subject = "Test Message",
Message = "This is an automation test",
IsHtml = false,
@@ -0,0 +1,19 @@
using LiteCharms.Features.TechShop.Products;
namespace LiteCharms.Features.TechShop.Tests;
public class ProductsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
[Fact]
public async Task GetProductsAsync_ReturnsProducts()
{
var productService = fixture.Services.GetRequiredService<ProductService>();
var result = await productService.GetProductsAsync();
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine($"Retrieved {result.Value.Length} products.");
}
}
@@ -0,0 +1,34 @@
{
"BookshopS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop",
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
},
"BookshopQuotesS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop.quotes",
"CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za"
},
"Email": {
"Credentials": {
"Username": "shop@litecharms.co.za"
},
"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,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Entities;
namespace LiteCharms.Features.TechShop.CartPackages.Entities;
[EntityTypeConfiguration<PackageConfirguration, Package>]
public class Package : Models.Package
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Entities;
namespace LiteCharms.Features.TechShop.CartPackages.Entities;
public class PackageConfirguration : IEntityTypeConfiguration<Package>
{
@@ -1,6 +1,6 @@
using LiteCharms.Features.Shop.Products.Entities;
using LiteCharms.Features.TechShop.Products.Entities;
namespace LiteCharms.Features.Shop.CartPackages.Entities;
namespace LiteCharms.Features.TechShop.CartPackages.Entities;
[EntityTypeConfiguration<PackageItemConfiguration, PackageItem>]
public class PackageItem : Models.PackageItem
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Entities;
namespace LiteCharms.Features.TechShop.CartPackages.Entities;
public class PackageItemConfiguration : IEntityTypeConfiguration<PackageItem>
{
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Models;
namespace LiteCharms.Features.TechShop.CartPackages.Models;
public class Package
{
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.CartPackages.Models;
namespace LiteCharms.Features.TechShop.CartPackages.Models;
public class PackageItem
{
@@ -1,10 +1,10 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.CartPackages.Models;
using LiteCharms.Features.Shop.Postgres;
using static LiteCharms.Features.Extensions.Timezones;
using LiteCharms.Features.TechShop.CartPackages.Models;
using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.Shop.CartPackages;
namespace LiteCharms.Features.TechShop.CartPackages;
public class PackageService(IDbContextFactory<ShopDbContext> contextFactory)
{
@@ -1,9 +1,9 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.Models;
using LiteCharms.Features.Shop.Customers.Models;
using LiteCharms.Features.Shop.Postgres;
using LiteCharms.Features.Models;
using LiteCharms.Features.TechShop.Customers.Models;
using LiteCharms.Features.TechShop.Extensions;
using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.Shop.Customers;
namespace LiteCharms.Features.TechShop.Customers;
public class CustomerService(IDbContextFactory<ShopDbContext> contextFactory)
{
@@ -1,9 +1,9 @@
using LiteCharms.Features.Shop.Leads.Entities;
using LiteCharms.Features.Shop.Orders.Entities;
using LiteCharms.Features.Shop.Quotes.Entities;
using LiteCharms.Features.Shop.ShoppingCarts.Entities;
using LiteCharms.Features.TechShop.Leads.Entities;
using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.TechShop.Quotes.Entities;
using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.Shop.Customers.Entities;
namespace LiteCharms.Features.TechShop.Customers.Entities;
[EntityTypeConfiguration<CustomerConfiguration, Customer>]
public class Customer : Models.Customer
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Customers.Entities;
namespace LiteCharms.Features.TechShop.Customers.Entities;
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Customers.Models;
namespace LiteCharms.Features.TechShop.Customers.Models;
public class Customer
{
@@ -1,4 +1,4 @@
namespace LiteCharms.Features.Shop.Customers.Models;
namespace LiteCharms.Features.TechShop.Customers.Models;
public record CreateCustomer
{
@@ -1,16 +1,4 @@
namespace LiteCharms.Features.Shop;
public enum EmailStatuses : int
{
GeneralError = 0,
AuthenticationError = 1,
ProtocolError = 2,
Connected = 3,
Disconnected = 4,
TooManyConnections = 5,
ConnectionAborted = 6,
Success = 7
}
namespace LiteCharms.Features.TechShop;
public enum CorrelationIdTypes : int
{
@@ -27,13 +15,6 @@ public enum CorrelationIdTypes : int
LinkedIn = 10
}
public enum Priorities : int
{
Low = 0,
Medium = 1,
High = 2,
}
public enum NotificationPlatforms : int
{
Email = 1,
@@ -0,0 +1,22 @@
using LiteCharms.Features.TechShop.HealthChecks;
using static LiteCharms.Features.TechShop.Extensions.Postgres;
namespace LiteCharms.Features.TechShop.Extensions;
public static class HealthChecks
{
public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<ShopQuartzHealthCheck>("ShopQuartz");
return services;
}
public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresShopHealthCheck>(TechShopDbConfigName);
return services;
}
}
@@ -1,17 +1,18 @@
using LiteCharms.Features.Shop.CartPackages.Models;
using LiteCharms.Features.Shop.Customers.Models;
using LiteCharms.Features.Shop.Leads.Models;
using LiteCharms.Features.Shop.Notifications.Models;
using LiteCharms.Features.Shop.Orders.Models;
using LiteCharms.Features.Shop.Products.Models;
using LiteCharms.Features.Shop.Quotes.Models;
using LiteCharms.Features.Shop.ShoppingCarts.Models;
using LiteCharms.Features.TechShop.CartPackages.Models;
using LiteCharms.Features.TechShop.Customers.Models;
using LiteCharms.Features.TechShop.Leads.Models;
using LiteCharms.Features.TechShop.Notifications.Models;
using LiteCharms.Features.TechShop.Orders.Models;
using LiteCharms.Features.TechShop.Products.Entities;
using LiteCharms.Features.TechShop.Products.Models;
using LiteCharms.Features.TechShop.Quotes.Models;
using LiteCharms.Features.TechShop.ShoppingCarts.Models;
namespace LiteCharms.Features.Extensions;
namespace LiteCharms.Features.TechShop.Extensions;
public static class EntityModeMappers
public static class Mappers
{
public static ShoppingCartPackage ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) =>
public static ShoppingCartPackage ToModel(this ShoppingCarts.Entities.ShoppingCartPackage entity) =>
new()
{
Id = entity.Id,
@@ -20,7 +21,7 @@ public static class EntityModeMappers
ShoppingCartId = entity.ShoppingCartId
};
public static PackageItem ToModel(this Features.Shop.CartPackages.Entities.PackageItem entity) =>
public static PackageItem ToModel(this CartPackages.Entities.PackageItem entity) =>
new()
{
Id = entity.Id,
@@ -30,7 +31,7 @@ public static class EntityModeMappers
ProductPriceId = entity.ProductPriceId
};
public static Package ToModel(this Features.Shop.CartPackages.Entities.Package entity) =>
public static Package ToModel(this CartPackages.Entities.Package entity) =>
new()
{
Id = entity.Id,
@@ -43,7 +44,7 @@ public static class EntityModeMappers
Summary = entity.Summary
};
public static ShoppingCartItem ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartItem entity) =>
public static ShoppingCartItem ToModel(this ShoppingCarts.Entities.ShoppingCartItem entity) =>
new()
{
Id = entity.Id,
@@ -54,7 +55,7 @@ public static class EntityModeMappers
ShoppingCartId = entity.ShoppingCartId
};
public static ShoppingCart ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCart entity) =>
public static ShoppingCart ToModel(this ShoppingCarts.Entities.ShoppingCart entity) =>
new()
{
Id = entity.Id,
@@ -64,7 +65,7 @@ public static class EntityModeMappers
OrderId = entity.OrderId
};
public static Quote ToModel(this Features.Shop.Quotes.Entities.Quote entity) =>
public static Quote ToModel(this Quotes.Entities.Quote entity) =>
new()
{
Id = entity.Id,
@@ -79,7 +80,7 @@ public static class EntityModeMappers
OrderId = entity.OrderId
};
public static Notification ToModel(this Features.Shop.Notifications.Entities.Notification entity) =>
public static Notification ToModel(this Notifications.Entities.Notification entity) =>
new()
{
Id = entity.Id,
@@ -103,7 +104,7 @@ public static class EntityModeMappers
Errors = entity.Errors
};
public static Customer ToModel(this Features.Shop.Customers.Entities.Customer entity) =>
public static Customer ToModel(this Customers.Entities.Customer entity) =>
new()
{
Id = entity.Id,
@@ -128,7 +129,7 @@ public static class EntityModeMappers
Whatsapp = entity.Whatsapp
};
public static Lead ToModel(this Features.Shop.Leads.Entities.Lead entity) =>
public static Lead ToModel(this Leads.Entities.Lead entity) =>
new()
{
Id = entity.Id,
@@ -149,7 +150,7 @@ public static class EntityModeMappers
Status = entity.Status
};
public static Order ToModel(this Features.Shop.Orders.Entities.Order entity) =>
public static Order ToModel(this Orders.Entities.Order entity) =>
new()
{
Id = entity.Id,
@@ -163,7 +164,7 @@ public static class EntityModeMappers
InvoiceUrl = entity.InvoiceUrl
};
public static OrderRefund ToModel(this Features.Shop.Orders.Entities.OrderRefund entity) =>
public static OrderRefund ToModel(this Orders.Entities.OrderRefund entity) =>
new()
{
Id = entity.Id,
@@ -173,19 +174,21 @@ public static class EntityModeMappers
Amount = entity.Amount
};
public static Product ToModel(this Features.Shop.Products.Entities.Product entity) =>
public static Products.Models.Product ToModel(this Products.Entities.Product entity) =>
new()
{
Id = entity.Id,
Id = entity.Id,
CreatedAt = entity.CreatedAt,
Name = entity.Name,
Description = entity.Description,
Active = entity.Active,
Summary = entity.Summary,
ImageUrl = entity.ImageUrl,
Thumbnails = entity.Thumbnails
Thumbnails = entity.Thumbnails,
Metadata = entity.Metadata,
};
public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) =>
public static Products.Models.ProductPrice ToModel(this Products.Entities.ProductPrice entity) =>
new()
{
Id = entity.Id,
@@ -0,0 +1,16 @@
using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.TechShop.Extensions;
public static class Postgres
{
public const string TechShopDbConfigName = "PostgresShop";
public static IServiceCollection AddTechShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<ShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(TechShopDbConfigName)));
return services;
}
}
@@ -0,0 +1,27 @@
using LiteCharms.Features.TechShop.CartPackages;
using LiteCharms.Features.TechShop.Customers;
using LiteCharms.Features.TechShop.Leads;
using LiteCharms.Features.TechShop.Notifications;
using LiteCharms.Features.TechShop.Orders;
using LiteCharms.Features.TechShop.Products;
using LiteCharms.Features.TechShop.Quotes;
using LiteCharms.Features.TechShop.ShoppingCarts;
namespace LiteCharms.Features.TechShop.Extensions;
public static class Shop
{
public static IServiceCollection AddTechShopServices(this IServiceCollection services)
{
services.AddSingleton<PackageService>()
.AddSingleton<LeadService>()
.AddSingleton<NotificationService>()
.AddSingleton<OrderService>()
.AddSingleton<ProductService>()
.AddSingleton<QuoteService>()
.AddSingleton<ShoppingCartService>()
.AddSingleton<CustomerService>();
return services;
}
}
@@ -1,8 +1,10 @@
namespace LiteCharms.Features.HealthChecks;
using static LiteCharms.Features.TechShop.Extensions.Postgres;
public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
namespace LiteCharms.Features.TechShop.HealthChecks;
public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck
{
private readonly string connectionString = configuration.GetConnectionString("PostgresShop")!;
private readonly string connectionString = configuration.GetConnectionString(TechShopDbConfigName)!;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
@@ -16,11 +18,11 @@ public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
await command.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy("PostgreSQL is responsive.");
return HealthCheckResult.Healthy($"{TechShopDbConfigName} is responsive.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex);
return HealthCheckResult.Unhealthy($"{TechShopDbConfigName} is unreachable.", ex);
}
}
}

Some files were not shown because too many files have changed in this diff Show More