Compare commits

..

11 Commits

Author SHA1 Message Date
khwezi 466458e230 Merge pull request 'Implemented Product Data Seeder' (#45) from midrandshop into master
Reviewed-on: #45
2026-05-29 18:56:45 +02:00
Khwezi Mngoma 60fcc70e98 Implemented Product Data Seeder
continuous-integration/drone/pr Build is passing
2026-05-29 18:56:08 +02:00
khwezi 141d32f591 Merge pull request 'Refactored migrations' (#44) from midrandshop into master
Reviewed-on: #44
2026-05-29 09:03:27 +02:00
Khwezi Mngoma 38e765203d Refactored migrations
continuous-integration/drone/pr Build is passing
2026-05-29 09:02:47 +02:00
khwezi d9e7f225ae Merge pull request 'midrandshop' (#43) from midrandshop into master
Reviewed-on: #43
2026-05-29 08:23:15 +02:00
Khwezi Mngoma 9eb3526a2e Moved Book Page DTOs
continuous-integration/drone/pr Build is passing
2026-05-29 08:21:11 +02:00
Khwezi Mngoma 4397976ed8 Implemented order service tests 2026-05-29 08:18:29 +02:00
Khwezi Mngoma 2546c34ffc Wrote tests for most services, applied EF core optimisations 2026-05-29 01:05:22 +02:00
Khwezi Mngoma 4e53ff8a37 ProductService tested and stable 2026-05-28 17:28:33 +02:00
Khwezi Mngoma 2a0b34c730 Refactored database registration to allow postgres to use internal representations for afster performance 2026-05-28 09:05:49 +02:00
khwezi 6ae63e2ad1 Merge pull request 'midrandshop' (#42) from midrandshop into master
Reviewed-on: #42
2026-05-27 09:15:44 +02:00
45 changed files with 1714 additions and 522 deletions
@@ -0,0 +1,8 @@
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
public class CdnSettings
{
public string? BaseCdn { get; set; }
public string[]? BookCovers { get; set; }
}
@@ -0,0 +1,158 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>5c3bc894-8654-4691-99e8-f90d3414843f</UserSecretsId>
</PropertyGroup>
<!-- Quartz Scheduler-->
<ItemGroup>
<PackageReference Include="Bogus" Version="35.6.5" />
<PackageReference Include="Meziantou.Analyzer" Version="3.0.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="Bogus" />
<Using Include="System.Globalization" />
<Using Include="System.Reflection" />
<Using Include="Microsoft.AspNetCore.Builder" />
<Using Include="Microsoft.Extensions.Hosting" />
<Using Include="System.Text" />
<Using Include="System.Text.Json" />
<Using Include="System.Threading.Channels" />
<Using Include="System.Collections.ObjectModel" />
<Using Include="System.Diagnostics" />
<Using Include="System.Diagnostics.Metrics" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="System.Security.Cryptography" />
<Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Features.MidrandBooks\LiteCharms.Features.MidrandBooks.csproj" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -0,0 +1,190 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks;
using LiteCharms.Features.MidrandBooks.Authors;
using LiteCharms.Features.MidrandBooks.Products;
using LiteCharms.Features.MidrandBooks.Seed.Configuration;
namespace LiteCharms.Features.MidrandBooks.Seed;
public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
{
private readonly CdnSettings cdnSettings = options.Value;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
logger.LogInformation("Product Seeding started");
if (cdnSettings.BookCovers is null || cdnSettings.BookCovers.Length == 0)
{
logger.LogWarning("No book covers found in CDN settings. Seeding aborted.");
return;
}
// Initialize Bogus Faker engine
var faker = new Faker();
var culture = CultureInfo.InvariantCulture;
// Ensure repeatable data sets if run multiple times by anchoring the seed
Randomizer.Seed = new Random(42);
foreach (var bookCover in cdnSettings.BookCovers)
{
if (stoppingToken.IsCancellationRequested) break;
// Generate beautifully mixed eclectic topics on the fly
var bookTopic = faker.PickRandom(
// --- Tech & IT ---
"C# 12 & Modern .NET Architecture",
"PostgreSQL Database Optimization",
"Docker & Kubernetes in Production",
"Domain-Driven Design Paradigms",
"Artificial Intelligence with Python",
// --- Sci-Fi & Fantasy ---
"The Chronicles of the Quantum Nebula",
"Legends of the Lost Cybernetic Kingdom",
"Parallel Dimensions and Rogue Time Streams",
"The Last Android in Neo-Johannesburg",
// --- Thrillers, Mystery & Crime ---
"The Midnight Code Cryptograph",
"Shadows in the Highveld",
"The Silent Witness of Midrand",
"Deception on the 14th Floor",
// --- Business, Finance & Wealth ---
"Mastering the South African Tech Market",
"The Modern Entrepreneur's Blueprint",
"Generational Wealth and Venture Capital",
"Negotiation Tactics for High-Stakes Deals",
// --- Self-Help & Personal Growth ---
"The Art of Relentless Focus",
"Building High-Performance Habits",
"The Mindfulness Guide for Software Engineers",
"Unlocking Creative Flow Under Pressure"
);
// Defensive Length Processing to avoid Entity Framework / Postgres string truncation crashes
var rawTitle = $"{faker.Company.CatchPhrase()} with {bookTopic}";
var bookTitle = rawTitle.Length > 255 ? rawTitle[..252] + "..." : rawTitle;
var rawSummary = $"A comprehensive guide to mastering {bookTopic}. Learn modern implementation techniques through real-world software engineering paradigms.";
var bookSummary = rawSummary.Length > 512 ? rawSummary[..509] + "..." : rawSummary;
// Generating a single concise paragraph ensures a rich text description falling safely well under 1024
var rawDescription = faker.Lorem.Paragraph(3);
var bookDescription = rawDescription.Length > 1024 ? rawDescription[..1021] + "..." : rawDescription;
var authorFirstName = faker.Name.FirstName();
var authorLastName = faker.Name.LastName();
var publisherCompany = faker.Company.CompanyName();
// Step 1: Add Product
var productCreateResult = await productService.CreateProductAsync(new Products.Models.CreateProduct
{
Name = bookTitle,
Summary = bookSummary,
Description = bookDescription,
ImageUrl = $"{cdnSettings.BaseCdn}{bookCover}",
Type = ProductTypes.Book,
Metadata = new Models.ProductMetadata
{
CopyrightInfo = $"© {DateTime.UtcNow.Year} {publisherCompany}. All rights reserved.",
ManufactureDate = faker.Date.Past(3).ToString("yyyy-MM-dd", culture),
Manufacturer = $"{authorFirstName} {authorLastName} / {publisherCompany}",
SerialNumber = faker.Phone.PhoneNumber("978-##########")
},
Categories = ["Coding", "Computers", "IT"]
}, stoppingToken);
if (productCreateResult.IsFailed)
{
logger.LogError("Failed to create product: {Error}", productCreateResult.Errors[0].Message);
break;
}
// Step 2: Enable product so it can show on the shop
var enableProductResult = await productService.UpdateProductStatusAsync(productId: productCreateResult.Value, isEnabled: true, stoppingToken);
if (enableProductResult.IsFailed)
{
logger.LogError("Failed to enable created product: {Error}", enableProductResult.Errors[0].Message);
break;
}
// Step 3: Create Product Price
var productPriceCreateResult = await productService.CreateProductPriceAsync(productId: productCreateResult.Value, request: new Products.Models.CreateProductPrice
{
// Generates fair, dynamic prices in Rands between R150 and R650, snapped neatly to integers
Amount = Math.Round(faker.Random.Decimal(150m, 650m), 2),
Discount = 0.0m
}, stoppingToken);
if (productPriceCreateResult.IsFailed)
{
logger.LogError("Failed to create product price: {Error}", productPriceCreateResult.Errors[0].Message);
break;
}
// Step 4: Create Author
var authorCreateResult = await authorService.CreateAuthorAsync(request: new Authors.Models.CreateAuthor
{
Name = authorFirstName,
LastName = authorLastName,
Company = publisherCompany,
VatNumber = faker.Random.Bool() ? faker.Phone.PhoneNumber("4#########") : "",
PublisherType = faker.PickRandom<PublisherTypes>(),
Email = faker.Internet.Email(authorFirstName, authorLastName),
Website = faker.Internet.Url(),
ImageUrl = faker.Internet.Avatar(),
SocialMedia =
[
new Models.SocialMedia
{
Name = "LinkedIn",
ImageUrl = "https://cdn.example.com/icons/linkedin.png",
Type = SocialMediaTypes.LinkedIn,
Url = $"https://linkedin.com/in/{authorFirstName.ToLower(culture)}-{authorLastName.ToLower(culture)}"
},
new Models.SocialMedia
{
Name = "GitHub",
ImageUrl = "https://cdn.example.com/icons/github.png",
Type = SocialMediaTypes.GitHub,
Url = $"https://github.com/tech-{authorFirstName.ToLower(culture)}"
}
],
Biography = $"{authorFirstName} {authorLastName} is a veteran technologist and systems architect with over a decade of domain expertise. " + faker.Lorem.Paragraph(2),
ThumbnailImageUrl = null
}, stoppingToken);
if (authorCreateResult.IsFailed)
{
logger.LogError("Failed to create author: {Error}", authorCreateResult.Errors[0].Message);
break;
}
// Step 5: Create Author-Book link (product linkage)
var authorBookCreateResult = await booksService.CreateBookAsync(authorId: authorCreateResult.Value, productId: productCreateResult.Value, stoppingToken);
if (authorBookCreateResult.IsFailed)
{
logger.LogError("Failed to create author-book linkage: {Error}", authorBookCreateResult.Errors[0].Message);
break;
}
var enableAuthorBookResult = await booksService.UpdateBookStatusAsync(bookId: authorBookCreateResult.Value, isEnabled: true, stoppingToken);
if (enableAuthorBookResult.IsFailed)
{
logger.LogError("Failed to enable author-book link: {Error}", enableAuthorBookResult.Errors[0].Message);
break;
}
logger.LogInformation("Successfully seeded book product: {Title}", bookTitle);
}
logger.LogInformation("Product Seeding completed successfully.");
}
}
@@ -0,0 +1,21 @@
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Seed;
using LiteCharms.Features.MidrandBooks.Seed.Configuration;
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration
.AddJsonFile("appsettings.json")
.AddUserSecrets(typeof(Program).Assembly);
builder.Services
.AddLogging()
.AddShopServices()
.AddHostedService<ProductsSeederService>()
.AddMidrandShopDatabase(builder.Configuration);
builder.Services.Configure<CdnSettings>(options => builder.Configuration.GetSection(nameof(CdnSettings)).Bind(options));
using var host = builder.Build();
await host.RunAsync();
@@ -0,0 +1,28 @@
{
"CdnSettings": {
"BaseCdn": "https://bookshop.cdn.khongisa.co.za/design/",
"BookCovers": [
"144e0314-0bd8-4e2c-8814-2b34608c0600_1764780116467.webp",
"2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp",
"3cd172b9-416e-4b3a-b613-7ff0a3aecc4c_1764780129459.webp",
"762947b6-63ac-436e-98fb-91dd94de1d82_1764780126141.webp",
"7b42f23f-666c-4dd9-82e1-9b928e1b4db7_1764780186376.webp",
"8e281eea-8910-473d-b650-43f539995d9e_1764780152316.webp",
"91d18e2c-6ee6-44b4-84a5-8692db5dbfe4_1764780114484.webp",
"94fdf403-4d10-4cb4-a537-fc914a1f5ba8_1764780198423.webp",
"9b881786-75e1-47f7-9bc9-27188d46d1ec_1764780122458.webp",
"c417236d-a628-45eb-82c2-ec1cb0326e4f_1765006317965.webp",
"clpqjsgi71jpr1i6392e449l2.webp",
"clps1mk9f0m1z1i32grvq17tg.webp",
"clq550xxy2dab1ywb6mdv4acv.webp",
"clq5535o52dj51yw557ml49c1.webp",
"clq55455w2dcm1yvd8d8258d8.webp",
"clq558fkh2djy1ywbghwcawnh.webp",
"clq55cvqh2dqi1yyratl123wq.webp",
"clrhnk94e115k1y00bg8h9ixb.webp",
"d44a3c04-f124-4f0b-8301-3841ae2fd439_1764780121224.webp",
"e6ba52f208914285bcdf1966cfb08f6f.jpg",
"fa9cbbe6-f947-4f83-8e98-61d2661f43e0_1764841636705.webp"
]
}
}
@@ -0,0 +1,81 @@
using LiteCharms.Features.MidrandBooks.Authors;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
[IntegrationFact]
public async Task CreateAuthorAsync_ShouldReturn_ResultWithAuthorId()
{
var request = new CreateAuthor
{
Name = "John",
LastName = "Doe",
Company = "Solo Publishers",
Email = "solo@publishers.co.za",
PublisherType = PublisherTypes.Independent,
ImageUrl = ""
};
var result = await authorService.CreateAuthorAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateAuthorAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateAuthor
{
Name = "Jane",
LastName = "Doe",
Company = "Solo Publishers",
Email = "solo@publishers.co.za",
PublisherType = PublisherTypes.Independent,
ImageUrl = ""
};
var result = await authorService.UpdateAuthorAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetAuthors_ShouldReturn_ResultWithAuthorList()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await authorService.GetAuthorsAsync(range, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetAuthorAsync_ShouldReturn_ResultWithAuthor()
{
var result = await authorService.GetAuthorAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task UpdateAuthorStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await authorService.UpdateAuthorStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -0,0 +1,52 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class BooksServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly BooksService bookService = fixture.Services.GetRequiredService<BooksService>();
[IntegrationFact]
public async Task CreateBookAsync_ShouldReturn_ResultWithBookId()
{
var result = await bookService.CreateBookAsync(1, 2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetBookAsync_ShouldReturn_ResultWithBook()
{
var result = await bookService.GetBookAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetBooksByAuthorAsync_ShouldReturn_ResultWithAuthorBooks()
{
var result = await bookService.GetBooksByAuthorAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetPublishedBooksAsync_ShouldReturn_ResultWithBublishedBooks()
{
var result = await bookService.GetPublishedBooksAsync(0, 1000, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task UpdateBookStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await bookService.UpdateBookStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -1,7 +1,8 @@
using LiteCharms.Features.Extensions; using LiteCharms.Features.Extensions;
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
namespace LiteCharms.Features.MidrandBooks.Tests; namespace LiteCharms.Features.MidrandBooks.Tests.Common;
public class Fixture : IDisposable public class Fixture : IDisposable
{ {
@@ -11,6 +12,10 @@ public class Fixture : IDisposable
public IMediator Mediator { get; set; } public IMediator Mediator { get; set; }
private readonly CancellationTokenSource cancellationTokenSource = new();
public CancellationToken CancellationToken => cancellationTokenSource.Token;
public Fixture() public Fixture()
{ {
Configuration = new ConfigurationBuilder() Configuration = new ConfigurationBuilder()
@@ -23,12 +28,12 @@ public class Fixture : IDisposable
Services = new ServiceCollection() Services = new ServiceCollection()
.AddMediator() .AddMediator()
.AddLogging() .AddLogging()
//.AddMidrandShopServices()
.AddEmailServiceBus() .AddEmailServiceBus()
.AddGarageS3(Configuration) .AddGarageS3(Configuration)
.AddMidrandShopDatabase(Configuration) .AddMidrandShopDatabase(Configuration)
.AddEmailServices(Configuration) .AddEmailServices(Configuration)
.AddSingleton(Configuration) .AddSingleton(Configuration)
.AddShopServices()
.BuildServiceProvider(); .BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>(); Mediator = Services.GetRequiredService<IMediator>();
@@ -0,0 +1,10 @@
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
public class IntegrationFactAttribute : FactAttribute
{
public IntegrationFactAttribute()
{
if(!Debugger.IsAttached)
Skip = "This test requires the debugger to be attached.";
}
}
@@ -0,0 +1,201 @@
using LiteCharms.Features.MidrandBooks.Customers;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class CustomerServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly CustomerService customerService = fixture.Services.GetRequiredService<CustomerService>();
[IntegrationFact]
public async Task CreateCustomerAsync_ShouldReturn_ResultWithCustomerId()
{
var request = new CreateCustomer
{
Company = "Book Lovers",
Email = "hank@booklovers.com",
Phone = "555 1245 8577",
Website = "https://www.booklovers.com"
};
var result = await customerService.CreateCustomerAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task CreateCustomerContactAsync_ShouldReturn_ResultWithCustomerContactId()
{
var request = new CreateCustomerContact
{
Name = "Sipho",
LastName = "Madlanga",
Phone = "0710857365",
Email = "sipho@madlanga.africa",
Type = ContactTypes.Business
};
var result = await customerService.CreateCustomerContactAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task CreateCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddressId()
{
var request = new CreateCustomerAddress
{
Name = "Business",
BuildingType = AddressBuildingTypes.MixedUse,
Type = AddressType.Shipping,
Street = "123 Building 4, XYZ Suburb, Some Region",
City = "Johannesburg",
State = "Gauteng",
Country = "South Africa",
IsPrimary = true,
Enabled = true,
PostalCode = "12345"
};
var result = await customerService.CreateCustomerAddressAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateCustomerAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomer
{
Company = "Book Lovers",
Email = "hank@booklovers.com",
Phone = "555 1245 8578",
Website = "https://www.booklovers.com"
};
var result = await customerService.UpdateCustomerAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerContactAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomerContact
{
Name = "Sipho",
LastName = "Madlanga",
Phone = "0710857366",
Email = "sipho@madlanga.africa",
Type = ContactTypes.Business
};
var result = await customerService.UpdateCustomerContactAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerAddressAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateCustomerAddress
{
Name = "Business",
BuildingType = AddressBuildingTypes.MixedUse,
Type = AddressType.Shipping,
Street = "123 Building 4, XYZ Suburb, Some Region",
City = "Johannesburg",
State = "Gauteng",
Country = "South Africa",
IsPrimary = true,
Enabled = true,
PostalCode = "12346"
};
var result = await customerService.UpdateCustomerAddressAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerStatusAsync(1, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerContactStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerContactStatusAsync(1, true, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateCustomerAddressStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await customerService.UpdateCustomerAddressStatusAsync(1, true, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetCustomersAsync_ShouldReturn_ResultWithCustomerList()
{
var result = await customerService.GetCustomersAsync(fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerContactsAsync_ShouldReturn_ResultWithCustomerContactList()
{
var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAddressesAsync_ShouldReturn_ResultWithCustomerAddressList()
{
var result = await customerService.GetCustomerAddressesAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAsync_ShouldReturn_ResultWithCustomer()
{
var result = await customerService.GetCustomerAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetCustomerContactAsync_ShouldReturn_ResultWithCustomerContact()
{
var result = await customerService.GetCustomerContactsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetCustomerAddressAsync_ShouldReturn_ResultWithCustomerAddress()
{
var result = await customerService.GetCustomerAddressAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
}
@@ -39,6 +39,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Using Include="System.Text.Json" />
<Using Include="System.Diagnostics" />
<Using Include="Xunit" /> <Using Include="Xunit" />
</ItemGroup> </ItemGroup>
@@ -0,0 +1,196 @@
using LiteCharms.Features.MidrandBooks.Orders;
using LiteCharms.Features.MidrandBooks.Orders.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class OrderServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly OrderService orderService = fixture.Services.GetRequiredService<OrderService>();
[IntegrationFact]
public async Task CreateOrderAsync_ShouldReturn_ResultWithOrderId()
{
var request = new CreateOrder(250, "At the intercomm, dial 1 then option 2");
var result = await orderService.CreateOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task AddItemToOrderAsync_ShouldReturn_ResultWithOrderItemId()
{
var request = new CreateOrderItem(1, 1, 2);
var result = await orderService.AddItemToOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task AddItemsToOrderAsync_ShouldReturn_ResultWithSuccess()
{
var requests = new List<CreateOrderItem>
{
new(1, 1, 1),
new(1, 1, 3)
};
var result = await orderService.AddItemsToOrderAsync(1, [.. requests], fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task RemoveItemFromOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.RemoveItemFromOrderAsync(1, 5, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task ClearOrderItemsAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.ClearOrderItemsAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CancelOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.CancelOrderAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetOrderAsync_ShouldReturn_ResultWithOrder()
{
var result = await orderService.GetOrderAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task GetOrdersByCustomerAsync_ShouldReturn_ResultWithOrderList()
{
var result = await orderService.GetOrdersByCustomerAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetOrdersAsync_ShouldReturn_ResultWithOrderList()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await orderService.GetOrdersAsync(range, 0, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task UpdateOrderStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateOrderStatusAsync(1, OrderStatus.Pending, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task AddShippingToOrderAsync_ShouldReturn_ResultWithSuccess()
{
var request = new CreateShipping(1, 2);
var result = await orderService.AddShippingToOrderAsync(1, request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task UpdateShippingStatusAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateShippingStatusAsync(1, ShippingStatuses.Shipped, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task GetShippingByOrderIdAsync_ShouldReturn_ResultWithShipping()
{
var result = await orderService.GetShippingByOrderIdAsync(1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task RemoveShippingFromOrderAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.RemoveShippingFromOrderAsync(1, 1, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateShippingTrackingNumberAsync_ShouldReturn_ResultWithSuccess()
{
var result = await orderService.UpdateShippingTrackingNumberAsync(1, 2, "NA0009969397");
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CreateShippingProviderAsync_ShouldReturn_ResultWithShippingProviderId()
{
var request = new CreateShippingProvider(ShippingProviderTypes.FastWay, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel");
var result = await orderService.CreateShippingProviderAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.True(result.Value > 0);
}
[IntegrationFact]
public async Task GetShippingProvidersAsync_ShouldReturn_ResultWithShippingProviderList()
{
var result = await orderService.GetShippingProvidersAsync(true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
}
[IntegrationFact]
public async Task GetShippingProviderAsync_ShouldReturn_ResultWithShippingProvider()
{
var result = await orderService.GetShippingProviderAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
}
[IntegrationFact]
public async Task UpdateShippingProviderAsync_ShouldReturn_ResultWithSuccess()
{
var request = new UpdateShippingProvider(2,true, "FastWay Couriers", 50, "https://www.fastway.co.za/our-services/track-your-parcel");
var result = await orderService.UpdateShippingProviderAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
}
@@ -0,0 +1,63 @@
using LiteCharms.Features.MidrandBooks.Pages;
using LiteCharms.Features.MidrandBooks.Tests.Common;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class PageServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
{
private readonly PageService pageService = fixture.Services.GetRequiredService<PageService>();
[IntegrationFact]
public async Task CreatePageAsync_ShouldReturn_ResultWithPageId()
{
}
[IntegrationFact]
public async Task GetPagesAsync_ByBookId_ShouldReturn_ResultWithPageList()
{
}
[IntegrationFact]
public async Task GetPageAsync_ShouldReturn_ResultWithPage()
{
}
[IntegrationFact]
public async Task GetPageByNumberAsync_ById_And_BookPageNumber_ShouldReturn_ResultWithPage()
{
}
[IntegrationFact]
public async Task UpdatePageAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeletePageAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeleteByPageTypeAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task DeleteAllAsync_ShouldReturn_ResultWithSuccess()
{
}
[IntegrationFact]
public async Task UpdatePageStatusAsync_ShouldReturn_ResultWithSuccess()
{
}
}
@@ -0,0 +1,143 @@
using LiteCharms.Features.MidrandBooks.Products;
using LiteCharms.Features.MidrandBooks.Products.Models;
using LiteCharms.Features.MidrandBooks.Tests.Common;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly ProductService productService = fixture.Services.GetRequiredService<ProductService>();
[IntegrationFact]
public async Task GetProductPriceAsync_ShouldReturn_RetultOneProductPrice()
{
var result = await productService.GetProductPriceAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductPricesAsync_ShouldReturn_RetultProductPriceList()
{
var result = await productService.GetProductPricesAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task SearchProductsAsync_ShouldReturn_RetultMatchingProducts()
{
var filter = new ProductFilter
{
Name = "system",
Manufacturer = "techwave",
SerialNumber = "2024",
MinPrice = 10,
MaxPrice = 30
};
var result = await productService.SearchProductsAsync(filter, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductAsync_ShouldReturn_RetultOneProduct()
{
var result = await productService.GetProductAsync(2, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task GetProductsAsync_ShouldReturn_RetultProducts()
{
var range = new DateRange
{
From = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(-7)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 1000
};
var result = await productService.GetProductsAsync(0, range, fixture.CancellationToken);
Assert.True(result.IsSuccess);
Assert.NotEmpty(result.Value);
output.WriteLine(JsonSerializer.Serialize(result.Value));
}
[IntegrationFact]
public async Task UpdateProductStatusAsync_ShouldReturn_ResultTrue()
{
var result = await productService.UpdateProductStatusAsync(2, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task UpdateProductPriceStatusAsync_ShouldReturn_ResultTrue()
{
var result = await productService.UpdateProductPriceStatusAsync(2, true, fixture.CancellationToken);
Assert.True(result.IsSuccess);
}
[IntegrationFact]
public async Task CreateProductPriceAsync_Should_Return_NewProductPriceId()
{
var request = new CreateProductPrice
{
Amount = 29.99m,
Discount = 0.00m
};
var result = await productService.CreateProductPriceAsync(2, request, fixture.CancellationToken);
Assert.True(result.IsSuccess, "Product price creation should be successful.");
Assert.True(result.Value > 0, "New ProductPriceId should be greater than 0.");
output.WriteLine($"Created ProductPriceId: {result.Value}");
}
[IntegrationFact]
public async Task CreateProductAsync_Result_Returns_ProductId()
{
var request = new CreateProduct
{
Name = "Systems Rewired",
Description = "[Design], <Code>, AND /CHAORS/ IN ***SYNC***",
Summary = "A comprehensive guide to systems thinking and design.",
ImageUrl = "https://bookshop.cdn.khongisa.co.za/design/2bf1f9a2-7b25-4fcf-9aa7-08941ea21e6c_1764838499686.webp",
Type = ProductTypes.Book,
Categories = ["Systems Thinking", "Design", "Programming"],
Metadata = new ProductMetadata
{
CopyrightInfo = "© 2024 John Doe. All rights reserved.",
ManufactureDate = "2024-06-01",
Manufacturer = "TechWave Publishing",
SerialNumber = "SR-2024-0001"
}
};
var result = await productService.CreateProductAsync(request, fixture.CancellationToken);
Assert.True(result.IsSuccess, "Product creation should be successful.");
Assert.True(result.Value > 0, "ProductId should be greater than 0.");
output.WriteLine($"Created ProductId: {result.Value}");
}
}
@@ -5,12 +5,6 @@
"BucketName": "bookshop", "BucketName": "bookshop",
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za" "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": { "Email": {
"Credentials": { "Credentials": {
"Username": "shop@litecharms.co.za" "Username": "shop@litecharms.co.za"
@@ -7,23 +7,21 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks;
public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{ {
public async ValueTask<Result> UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken) public async ValueTask<Result> UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken = default)
{ {
try try
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var book = await context.Books.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); var rowsUpdated = await context.Books
.Where(b => b.Id == bookId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(b => b.Enabled, isEnabled)
.SetProperty(b => b.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (book is null) return rowsUpdated > 0
return Result.Fail(new Error($"Book with ID {bookId} not found"));
book.UpdatedAt = DateTime.UtcNow;
book.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error($"Failed to change status of book with ID {bookId}")); : Result.Fail(new Error($"Book with ID {bookId} not found"));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -45,8 +43,9 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
var book = context.Books.Add(new Entities.AuthorBook var book = context.Books.Add(new Entities.AuthorBook
{ {
CreatedAt = DateTime.UtcNow,
AuthorId = authorId, AuthorId = authorId,
ProductId = productId, ProductId = productId
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
@@ -68,7 +67,8 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
var book = await context.Books var book = await context.Books
.AsNoTracking() .AsNoTracking()
.Include(b => b.Author) .Include(b => b.Author)
.Include(b => b.Product!.Price) .Include(b => b.Product)
.ThenInclude(b => b!.Prices)
.Include(b => b.Pages) .Include(b => b.Pages)
.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken); .FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken);
@@ -86,7 +86,7 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
{ {
try try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken)) if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken))
return Result.Fail<AuthorBook[]>(new Error($"Author with ID {authorId} not found")); return Result.Fail<AuthorBook[]>(new Error($"Author with ID {authorId} not found"));
@@ -94,7 +94,8 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
var books = await context.Books var books = await context.Books
.AsNoTracking() .AsNoTracking()
.Include(b => b.Author) .Include(b => b.Author)
.Include(b => b.Product!.Price) .Include(b => b.Product)
.ThenInclude(b => b.Prices)
.OrderByDescending(b => b.CreatedAt) .OrderByDescending(b => b.CreatedAt)
.Where(b => b.AuthorId == authorId) .Where(b => b.AuthorId == authorId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -118,9 +119,10 @@ public sealed class BooksService(IDbContextFactory<MidrandBooksDbContext> contex
var books = await context.Books var books = await context.Books
.AsNoTracking() .AsNoTracking()
.Include(b => b.Author) .Include(b => b.Author)
.Include(b => b.Product!.Price) .Include(b => b.Product)
.ThenInclude(b => b!.Prices)
.Include(b => b.Pages) .Include(b => b.Pages)
.Where(b => b.Enabled && b.Product!.Enabled && b.Author.Enabled) .Where(b => b.Enabled && b.Product!.Enabled && b.Author!.Enabled)
.OrderByDescending(b => b.Ranking) .OrderByDescending(b => b.Ranking)
.ThenByDescending(b => b.Ranking) .ThenByDescending(b => b.Ranking)
.ThenByDescending(b => b.CreatedAt) .ThenByDescending(b => b.CreatedAt)
@@ -6,7 +6,7 @@ namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
public class AuthorBook : Models.AuthorBook public class AuthorBook : Models.AuthorBook
{ {
public virtual Author Author { get; set; } = new(); public virtual Author? Author { get; set; }
public new virtual Product? Product { get; set; } public new virtual Product? Product { get; set; }
@@ -1,5 +1,4 @@
using LiteCharms.Features.MidrandBooks.Abstractions; using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Authors.Models; using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Extensions; using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
@@ -9,53 +8,21 @@ namespace LiteCharms.Features.MidrandBooks.Authors;
public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{ {
public async ValueTask<Result<AuthorBook[]>> GetAuthorBooksAsync(long authorId, CancellationToken cancellationToken) public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken = default)
{ {
try try
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); var rowsUpdated = await context.Authors
.Where(a => a.Id == authorId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(a => a.Enabled, isEnabled)
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (author is null) return rowsUpdated > 0
return Result.Fail<AuthorBook[]>(new Error($"Author with ID {authorId} not found"));
var books = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.OrderByDescending(b => b.CreatedAt)
.Where(p => p.AuthorId == authorId)
.AsSplitQuery()
.ToArrayAsync(cancellationToken);
return books?.Length > 0
? Result.Ok(books.Select(b => b.ToModel()).ToArray())
: Result.Fail<AuthorBook[]>(new Error($"No books found for author with ID {authorId}"));
}
catch (Exception ex)
{
return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateAuthorStatusAsync(long authorId, bool isEnabled, CancellationToken cancellationToken)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
if (author is null)
return Result.Fail(new Error($"Author with ID {authorId} not found"));
author.UpdatedAt = DateTime.UtcNow;
author.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error($"Failed to change status of author with ID {authorId}")); : Result.Fail(new Error($"Author with ID {authorId} not found"));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -81,12 +48,12 @@ public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> conte
} }
} }
public async ValueTask<Result<Author[]>> GetAuthors(DateRange range, CancellationToken cancellationToken) public async ValueTask<Result<Author[]>> GetAuthorsAsync(DateRange range, CancellationToken cancellationToken = default)
{ {
try try
{ {
var fromDate = range.From.ToDateTime(TimeOnly.MinValue); var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue); var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -107,18 +74,12 @@ public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> conte
} }
} }
public async ValueTask<Result> UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken) public async ValueTask<Result> UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken = default)
{ {
try try
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken))
return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists"));
if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken))
return Result.Fail(new Error($"An author with the email {request.Email} already exists"));
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken); var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
if (author is null) if (author is null)
@@ -151,12 +112,12 @@ public sealed class AuthorService(IDbContextFactory<MidrandBooksDbContext> conte
{ {
try try
{ {
using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if(await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken)) if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken))
return Result.Fail<long>(new Error($"An author with the name {request.Name} {request.LastName} already exists")); return Result.Fail<long>(new Error($"An author with the name {request.Name} {request.LastName} already exists"));
if(await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken)) if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken))
return Result.Fail<long>(new Error($"An author with the email {request.Email} already exists")); return Result.Fail<long>(new Error($"An author with the email {request.Email} already exists"));
var newAuthor = context.Authors.Add(new Entities.Author var newAuthor = context.Authors.Add(new Entities.Author
@@ -18,7 +18,8 @@ public sealed class AuthorConfiguration : IEntityTypeConfiguration<Author>
builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024); builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048); builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048);
builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048); builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048);
builder.Property(f => f.SocialMedia).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Enabled).HasDefaultValue(true); builder.Property(f => f.Enabled).HasDefaultValue(true);
builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); });
} }
} }
@@ -30,7 +30,7 @@ public class Author
public string? ThumbnailImageUrl { get; set; } public string? ThumbnailImageUrl { get; set; }
public SocialMedia[]? SocialMedia { get; set; } public ICollection<SocialMedia>? SocialMedia { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
} }
@@ -13,7 +13,7 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Customers.AnyAsync(c => c.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) if (await context.Customers.AnyAsync(c => EF.Functions.ILike(c.Email!, $"%{request.Email}%"), cancellationToken))
return Result.Fail<long>(new Error($"Customer with email '{request.Email}' already exists.")); return Result.Fail<long>(new Error($"Customer with email '{request.Email}' already exists."));
var customer = context.Customers.Add(new Entities.Customer var customer = context.Customers.Add(new Entities.Customer
@@ -46,7 +46,7 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken)) if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
return Result.Fail<long>(new Error($"Customer with ID '{customerId}' does not exist.")); return Result.Fail<long>(new Error($"Customer with ID '{customerId}' does not exist."));
if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && cc.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken)) if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && EF.Functions.ILike(cc.Email!, $"%{request.Email}%"), cancellationToken))
return Result.Fail<long>(new Error($"Contact with email '{request.Email}' already exists for this customer.")); return Result.Fail<long>(new Error($"Contact with email '{request.Email}' already exists for this customer."));
var contact = context.Contacts.Add(new Entities.Contact var contact = context.Contacts.Add(new Entities.Contact
@@ -139,21 +139,19 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); var rowsUpdated = await context.Contacts
.Where(cc => cc.Id == contactId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(cc => cc.Name, request.Name)
.SetProperty(cc => cc.LastName, request.LastName)
.SetProperty(cc => cc.Email, request.Email)
.SetProperty(cc => cc.Phone, request.Phone)
.SetProperty(cc => cc.Type, request.Type)
.SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (contact is null) return rowsUpdated > 0
return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
contact.UpdatedAt = DateTime.UtcNow;
contact.Name = request.Name;
contact.LastName = request.LastName;
contact.Email = request.Email;
contact.Phone = request.Phone;
contact.Type = request.Type;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error("Failed to update customer contact.")); : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -167,25 +165,23 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); var rowsUpdated = await context.Addresses
.Where(a => a.Id == addressId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(a => a.Street, request.Street)
.SetProperty(a => a.City, request.City)
.SetProperty(a => a.State, request.State)
.SetProperty(a => a.PostalCode, request.PostalCode)
.SetProperty(a => a.Country, request.Country)
.SetProperty(a => a.Type, request.Type)
.SetProperty(a => a.BuildingType, request.BuildingType)
.SetProperty(a => a.IsPrimary, request.IsPrimary)
.SetProperty(a => a.Name, request.Name)
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (address is null) return rowsUpdated > 0
return Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
address.UpdatedAt = DateTime.UtcNow;
address.Street = request.Street;
address.City = request.City;
address.State = request.State;
address.PostalCode = request.PostalCode;
address.Country = request.Country;
address.Type = request.Type;
address.BuildingType = request.BuildingType;
address.IsPrimary = request.IsPrimary;
address.Name = request.Name;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error("Failed to update customer address.")); : Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -199,17 +195,15 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken); var rowsUpdated = await context.Customers
.Where(c => c.Id == customerId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(c => c.Enabled, enabled)
.SetProperty(c => c.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (customer is null) return rowsUpdated > 0
return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist."));
customer.Enabled = enabled;
customer.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error("Failed to update customer status.")); : Result.Fail(new Error($"Customer with ID '{customerId}' does not exist."));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -223,18 +217,16 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken); var rowsUpdated = await context.Contacts
.Where(cc => cc.Id == contactId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(cc => cc.Enabled, enabled)
.SetProperty(cc => cc.IsPrimary, isPrimary)
.SetProperty(cc => cc.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (contact is null) return rowsUpdated > 0
return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
contact.Enabled = enabled;
contact.IsPrimary = isPrimary;
contact.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error("Failed to update customer contact status.")); : Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -248,18 +240,16 @@ public sealed class CustomerService(IDbContextFactory<MidrandBooksDbContext> con
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken); var rowsUpdated = await context.Addresses
.Where(a => a.Id == addressId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(a => a.Enabled, enabled)
.SetProperty(a => a.IsPrimary, isPrimary)
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (address is null) return rowsUpdated > 0
return Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
address.Enabled = enabled;
address.IsPrimary = isPrimary;
address.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error("Failed to update customer address status.")); : Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -14,7 +14,8 @@ public sealed class CustomerConfiguration : IEntityTypeConfiguration<Customer>
builder.Property(c => c.Email).IsRequired(); builder.Property(c => c.Email).IsRequired();
builder.Property(c => c.Phone).IsRequired(); builder.Property(c => c.Phone).IsRequired();
builder.Property(c => c.Website).IsRequired(); builder.Property(c => c.Website).IsRequired();
builder.Property(c => c.SocialMedia).IsRequired(false).HasColumnType("jsonb");
builder.Property(c => c.Enabled).HasDefaultValue(true); builder.Property(c => c.Enabled).HasDefaultValue(true);
builder.OwnsMany(f => f.SocialMedia, b => { b.ToJson(); });
} }
} }
@@ -20,7 +20,7 @@ public class Customer
public string? Phone { get; set; } public string? Phone { get; set; }
public SocialMedia[]? SocialMedia { get; set; } public ICollection<SocialMedia>? SocialMedia { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
} }
@@ -14,7 +14,7 @@ public record CreateCustomer
public string? Phone { get; set; } public string? Phone { get; set; }
public SocialMedia[]? SocialMedia { get; set; } public ICollection<SocialMedia>? SocialMedia { get; set; }
} }
public sealed record UpdateCustomer : CreateCustomer; public sealed record UpdateCustomer : CreateCustomer;
@@ -17,7 +17,8 @@ public static class Mappers
Name = entity.Name, Name = entity.Name,
Type = entity.Type, Type = entity.Type,
Price = entity.Price, Price = entity.Price,
Enabled = entity.Enabled Enabled = entity.Enabled,
TrackingUrl = entity.TrackingUrl,
}; };
public static Shipping ToModel(this Orders.Entities.Shipping entity) => new() public static Shipping ToModel(this Orders.Entities.Shipping entity) => new()
@@ -8,8 +8,18 @@ public static class Postgres
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration) 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 => services.AddPooledDbContextFactory<MidrandBooksDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName))); options.UseNpgsql(dataSource));
return services; return services;
} }
@@ -4,19 +4,15 @@ namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class Shop public static class Shop
{ {
public static IServiceCollection AddShopServices(this IServiceCollection services, Assembly assembly, ServiceLifetime serviceLifetime) public static IServiceCollection AddShopServices(this IServiceCollection services)
{ {
var serviceType = typeof(IService); var serviceType = typeof(IService);
var implementations = assembly.GetTypes() var implementations = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract); .Where(t => serviceType.IsAssignableFrom(t) && t.IsClass && !t.IsAbstract);
foreach (var implementation in implementations) foreach (var implementation in implementations)
{ services.AddScoped(implementation);
var descriptor = new ServiceDescriptor(serviceType, implementation, serviceLifetime);
services.Add(descriptor);
}
return services; return services;
} }
@@ -11,7 +11,7 @@ public sealed class OrderConfiguration : IEntityTypeConfiguration<Order>
builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()"); builder.Property(o => o.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()"); builder.Property(o => o.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(o => o.Status).IsRequired(); builder.Property(o => o.Status).IsRequired();
builder.Property(o => o.Total).IsRequired().HasColumnType("decimal(18,2)"); builder.Property(o => o.Total).IsRequired().HasPrecision(18, 2);
builder.Property(o => o.Notes).HasMaxLength(1000); builder.Property(o => o.Notes).HasMaxLength(1000);
} }
} }
@@ -1,10 +1,10 @@
namespace LiteCharms.Features.MidrandBooks.Orders.Models; namespace LiteCharms.Features.MidrandBooks.Orders.Models;
public sealed record CreateOrder(long CustomerId, decimal TotalPrice, string? Notes); public sealed record CreateOrder(decimal TotalPrice, string? Notes);
public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity); public sealed record CreateOrderItem(long AuthorBookId, long ProductPriceId, int Quantity);
public sealed record CreateShipping(long OrderId, long AddressId, long ShippingProviderId, string? TrackingNumber); public sealed record CreateShipping(long AddressId, long ShippingProviderId, string? TrackingNumber = null);
public sealed record CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl); public sealed record CreateShippingProvider(ShippingProviderTypes Type, string Name, decimal Price, string TrackingUrl);
@@ -49,6 +49,17 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken)) if (!await context.Prices.AnyAsync(pp => pp.Id == request.ProductPriceId, cancellationToken))
return Result.Fail<long>("Product price not found."); 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 var orderItem = context.OrderItems.Add(new Entities.OrderItem
{ {
OrderId = orderId, OrderId = orderId,
@@ -78,9 +89,6 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) if (!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken))
return Result.Fail("Order not found."); return Result.Fail("Order not found.");
var existingItems = context.OrderItems.Where(oi => oi.OrderId == orderId);
context.OrderItems.RemoveRange(existingItems);
foreach (var item in items) foreach (var item in items)
{ {
@@ -90,18 +98,23 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken)) if (!await context.Prices.AnyAsync(pp => pp.Id == item.ProductPriceId, cancellationToken))
return Result.Fail($"Product price with ID {item.ProductPriceId} not found."); return Result.Fail($"Product price with ID {item.ProductPriceId} not found.");
context.OrderItems.Add(new Entities.OrderItem var existingItem = await context.OrderItems.FirstOrDefaultAsync(i => i.ProductPriceId == item.ProductPriceId && i.OrderId == orderId, cancellationToken);
{
OrderId = orderId, if (existingItem is not null)
AuthorBookId = item.AuthorBookId, existingItem.Quantity += item.Quantity;
ProductPriceId = item.ProductPriceId, else
Quantity = item.Quantity context.OrderItems.Add(new Entities.OrderItem
}); {
OrderId = orderId,
AuthorBookId = item.AuthorBookId,
ProductPriceId = item.ProductPriceId,
Quantity = item.Quantity
});
await context.SaveChangesAsync(cancellationToken);
} }
return await context.SaveChangesAsync(cancellationToken) > 0 return Result.Ok();
? Result.Ok()
: Result.Fail("Failed to add items to order.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -115,16 +128,13 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var orderItem = await context.OrderItems.FirstOrDefaultAsync(oi => oi.Id == orderItemId && oi.OrderId == orderId, cancellationToken); var rowsDeleted = await context.OrderItems
.Where(oi => oi.Id == orderItemId && oi.OrderId == orderId)
.ExecuteDeleteAsync(cancellationToken);
if (orderItem is null) return rowsDeleted > 0
return Result.Fail("Order item not found.");
context.OrderItems.Remove(orderItem);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to remove item from order."); : Result.Fail("Order item not found or failed to remove.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -132,15 +142,14 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
} }
} }
public async ValueTask<Result> ClearOrderItemasAsync(long orderId, CancellationToken cancellationToken = default) public async ValueTask<Result> ClearOrderItemsAsync(long orderId, CancellationToken cancellationToken = default)
{ {
try try
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var orderItems = context.OrderItems.Where(oi => oi.OrderId == orderId); var deletedItems = await context.OrderItems.Where(oi => oi.OrderId == orderId)
.ExecuteDeleteAsync(cancellationToken);
context.OrderItems.RemoveRange(orderItems);
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
@@ -199,12 +208,15 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{ {
try 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); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var orders = await context.Orders var orders = await context.Orders
.AsNoTracking() .AsNoTracking()
.Where(o => o.CreatedAt >= range.From.ToDateTime(TimeOnly.MinValue) && o.CreatedAt <= range.To.ToDateTime(TimeOnly.MaxValue)) .Where(o => o.CreatedAt >= fromDate && o.CreatedAt <= toDate)
.Skip(index * range.MaxRecords) .Skip(index).Take(range.MaxRecords)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return Result.Ok(orders.Select(o => o.ToModel()).ToArray()); return Result.Ok(orders.Select(o => o.ToModel()).ToArray());
@@ -221,17 +233,15 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var order = await context.Orders.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken); var rowsUpdated = await context.Orders
.Where(o => o.Id == orderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(o => o.Status, newStatus)
.SetProperty(o => o.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (order is null) return rowsUpdated > 0
return Result.Fail("Order not found.");
order.UpdatedAt = DateTime.UtcNow;
order.Status = newStatus;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to update order status."); : Result.Fail("Order not found or status update failed.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -239,7 +249,7 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
} }
} }
public async ValueTask<Result> AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default) public async ValueTask<Result<long>> AddShippingToOrderAsync(long orderId, CreateShipping request, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -282,17 +292,16 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId, cancellationToken); var rowsUpdated = await context.Shippings
.Where(s => s.OrderId == orderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(s => s.Status, newStatus)
.SetProperty(s => s.UpdatedAt, DateTime.UtcNow),
cancellationToken);
if (shipping is null) return rowsUpdated > 0
return Result.Fail("Shipping not found for this order.");
shipping.UpdatedAt = DateTime.UtcNow;
shipping.Status = newStatus;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to update shipping status."); : Result.Fail("Shipping not found for this order or status update failed.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -325,21 +334,14 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
try try
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var rowsDeleted = await context.Shippings
.Where(s => s.Id == shippingId && s.OrderId == orderId)
.ExecuteDeleteAsync(cancellationToken);
if(!await context.Orders.AnyAsync(o => o.Id == orderId, cancellationToken)) return rowsDeleted > 0
return Result.Fail("Order not found.");
var shipping = await context.Shippings.AsNoTracking()
.FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken);
if (shipping is null)
return Result.Fail("Shipping not found for this order.");
context.Shippings.Remove(shipping);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to remove shipping from order."); : Result.Fail("Shipping record not found for this order.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -353,17 +355,15 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var shipping = await context.Shippings.FirstOrDefaultAsync(s => s.OrderId == orderId && s.Id == shippingId, cancellationToken); var rowsUpdated = await context.Shippings
.Where(s => s.Id == shippingId && s.OrderId == orderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(s => s.TrackingNumber, trackingNumber)
.SetProperty(s => s.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (shipping is null) return rowsUpdated > 0
return Result.Fail("Shipping not found for this order.");
shipping.UpdatedAt = DateTime.UtcNow;
shipping.TrackingNumber = trackingNumber;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to update shipping tracking number."); : Result.Fail("Shipping record not found for this order.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -371,7 +371,7 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
} }
} }
public async ValueTask<Result> CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default) public async ValueTask<Result<long>> CreateShippingProviderAsync(CreateShippingProvider request, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -382,6 +382,7 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider var shippingProvider = context.ShippingProviders.Add(new Entities.ShippingProvider
{ {
CreatedAt = DateTime.UtcNow,
Name = request.Name, Name = request.Name,
Type = request.Type, Type = request.Type,
Price = request.Price, Price = request.Price,
@@ -389,7 +390,7 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
}); });
return await context.SaveChangesAsync(cancellationToken) > 0 return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok(shippingProvider.Entity.Id)
: Result.Fail("Failed to create shipping provider."); : Result.Fail("Failed to create shipping provider.");
} }
catch (Exception ex) catch (Exception ex)
@@ -440,24 +441,22 @@ public sealed class OrderService(IDbContextFactory<MidrandBooksDbContext> contex
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var provider = await context.ShippingProviders.FirstOrDefaultAsync(sp => sp.Id == request.ProviderId, cancellationToken); var rowsUpdated = await context.ShippingProviders
.Where(sp => sp.Id == request.ProviderId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(sp => sp.Name, request.Name)
.SetProperty(sp => sp.Price, request.Price)
.SetProperty(sp => sp.TrackingUrl, request.TrackingUrl)
.SetProperty(sp => sp.Enabled, request.Enabled)
.SetProperty(sp => sp.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (provider is null) return rowsUpdated > 0
return Result.Fail("Shipping provider not found.");
provider.UpdatedAt = DateTime.UtcNow;
provider.Enabled = request.Enabled;
provider.Name = request.Name;
provider.Price = request.Price;
provider.TrackingUrl = request.TrackingUrl;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to update shipping provider status."); : Result.Fail("Shipping provider not found.");
} }
catch (Exception ex) catch (Exception ex)
{ {
return Result.Fail(new Error(ex.Message).CausedBy(ex)); return Result.Fail(new Error(ex.Message).CausedBy(ex));
} }
} }
} }
@@ -13,10 +13,11 @@ public sealed class BookPageConfiguration : IEntityTypeConfiguration<BookPage>
builder.Property(bp => bp.AuthorBookId).IsRequired(); builder.Property(bp => bp.AuthorBookId).IsRequired();
builder.Property(bp => bp.Content).IsRequired(); builder.Property(bp => bp.Content).IsRequired();
builder.Property(bp => bp.Type).IsRequired(); builder.Property(bp => bp.Type).IsRequired();
builder.Property(bp => bp.ContentType).IsRequired(); builder.Property(bp => bp.ContentType).IsRequired();
builder.Property(bp => bp.Notes).IsRequired(false).HasColumnType("jsonb");
builder.Property(bp => bp.References).IsRequired(false).HasColumnType("jsonb");
builder.Property(bp => bp.Enabled).HasDefaultValue(true); 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) builder.HasOne(f =>f.Book)
.WithMany(b => b.Pages) .WithMany(b => b.Pages)
@@ -22,7 +22,7 @@ public class BookPage
public string[]? Notes { get; set; } public string[]? Notes { get; set; }
public PageReference[]? References { get; set; } public ICollection<PageReference>? References { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
} }
@@ -14,5 +14,7 @@ public class CreateBookPage
public string[]? Notes { get; set; } public string[]? Notes { get; set; }
public PageReference[]? References { get; set; } public ICollection<PageReference>? References { get; set; }
} }
public sealed class UpdateBookPage : CreateBookPage;
@@ -1,3 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
public sealed class UpdateBookPage : CreateBookPage;
@@ -13,19 +13,13 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken)) var rowsDeleted = await context.Pages
return Result.Fail("Book not found"); .Where(p => p.AuthorBookId == authorBookId)
.ExecuteDeleteAsync(cancellationToken);
var pages = await context.Pages.Where(p => p.AuthorBookId == authorBookId).ToListAsync(cancellationToken); return rowsDeleted > 0
? Result.Ok()
if (pages.Count == 0) : Result.Fail("No pages found for the specified book");
return Result.Fail("No pages found for the specified book");
context.Pages.RemoveRange(pages);
await context.SaveChangesAsync(cancellationToken);
return Result.Ok();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -39,16 +33,13 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType, cancellationToken); var rowsDeleted = await context.Pages
.Where(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType)
.ExecuteDeleteAsync(cancellationToken);
if (page is null) return rowsDeleted > 0
return Result.Fail("Page not found");
context.Pages.Remove(page);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to delete page"); : Result.Fail("Page not found");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -62,17 +53,15 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); var rowsUpdated = await context.Pages
.Where(p => p.Id == bookPageId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Enabled, enabled)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (page is null) return rowsUpdated > 0
return Result.Fail("Page not found");
page.UpdatedAt = DateTime.UtcNow;
page.Enabled = enabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to update page status"); : Result.Fail("Page not found");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -86,16 +75,13 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); var rowsDeleted = await context.Pages
.Where(p => p.Id == bookPageId)
.ExecuteDeleteAsync(cancellationToken);
if (page is null) return rowsDeleted > 0
return Result.Fail("Page not found");
context.Pages.Remove(page);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to delete page"); : Result.Fail("Page not found");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -109,22 +95,20 @@ public sealed class PageService(IDbContextFactory<MidrandBooksDbContext> context
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken); var rowsUpdated = await context.Pages
.Where(p => p.Id == bookPageId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Type, request.Type)
.SetProperty(p => p.ContentType, request.ContentType)
.SetProperty(p => p.Number, request.Number)
.SetProperty(p => p.Content, request.Content)
.SetProperty(p => p.Notes, request.Notes)
.SetProperty(p => p.References, request.References)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (page is null) return rowsUpdated > 0
return Result.Fail("Page not found");
page.UpdatedAt = DateTime.UtcNow;
page.Type = request.Type;
page.ContentType = request.ContentType;
page.Number = request.Number;
page.Content = request.Content;
page.Notes = request.Notes;
page.References = request.References;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail("Failed to update page"); : Result.Fail("Page not found");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -1,7 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@@ -13,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
{ {
[DbContext(typeof(MidrandBooksDbContext))] [DbContext(typeof(MidrandBooksDbContext))]
[Migration("20260527070840_Init")] [Migration("20260529070104_Init")]
partial class Init partial class Init
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -112,9 +111,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Property<int>("PublisherType") b.Property<int>("PublisherType")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<string>("ThumbnailImageUrl") b.Property<string>("ThumbnailImageUrl")
.HasMaxLength(2048) .HasMaxLength(2048)
.HasColumnType("character varying(2048)"); .HasColumnType("character varying(2048)");
@@ -291,9 +287,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -338,7 +331,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<decimal>("Total") b.Property<decimal>("Total")
.HasColumnType("decimal(18,2)"); .HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -494,17 +488,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasDefaultValue(true); .HasDefaultValue(true);
b.PrimitiveCollection<string>("Notes") b.PrimitiveCollection<string[]>("Notes")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<int>("Number") b.Property<int>("Number")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasDefaultValue(0); .HasDefaultValue(0);
b.Property<PageReference[]>("References")
.HasColumnType("jsonb");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -570,8 +561,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.PrimitiveCollection<string>("Categories") b.PrimitiveCollection<string[]>("Categories")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -591,9 +582,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(1024) .HasMaxLength(1024)
.HasColumnType("character varying(1024)"); .HasColumnType("character varying(1024)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(255) .HasMaxLength(255)
@@ -604,8 +592,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("character varying(512)"); .HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("ThumbnailUrls") b.PrimitiveCollection<string[]>("ThumbnailUrls")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -661,40 +649,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.ToTable("Prices", (string)null); b.ToTable("Prices", (string)null);
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Discount")
.HasColumnType("numeric");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<long>("ProductId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId")
.IsUnique();
b.ToTable("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author")
@@ -714,6 +668,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Product"); 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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
@@ -736,6 +722,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Customer"); 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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook")
@@ -798,7 +816,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.OnDelete(DeleteBehavior.NoAction) .OnDelete(DeleteBehavior.NoAction)
.IsRequired(); .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("Book");
b.Navigation("References");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
@@ -812,6 +857,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Order"); 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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
@@ -823,15 +897,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Product"); b.Navigation("Product");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null)
.WithOne("Price")
.HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{ {
b.Navigation("Pages"); b.Navigation("Pages");
@@ -865,8 +930,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{ {
b.Navigation("Price");
b.Navigation("Prices"); b.Navigation("Prices");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
@@ -1,5 +1,4 @@
using System; using System;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
@@ -31,8 +30,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Website = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), Website = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false), ImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
ThumbnailImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true), ThumbnailImageUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
SocialMedia = table.Column<SocialMedia[]>(type: "jsonb", nullable: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true) SocialMedia = table.Column<string>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -52,8 +51,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Email = table.Column<string>(type: "text", nullable: false), Email = table.Column<string>(type: "text", nullable: false),
Website = table.Column<string>(type: "text", nullable: false), Website = table.Column<string>(type: "text", nullable: false),
Phone = table.Column<string>(type: "text", nullable: false), Phone = table.Column<string>(type: "text", nullable: false),
SocialMedia = table.Column<SocialMedia[]>(type: "jsonb", nullable: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true) SocialMedia = table.Column<string>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -70,7 +69,7 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"), UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true, defaultValueSql: "now()"),
CustomerId = table.Column<long>(type: "bigint", nullable: false), CustomerId = table.Column<long>(type: "bigint", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false), Status = table.Column<int>(type: "integer", nullable: false),
Total = table.Column<decimal>(type: "numeric(18,2)", nullable: false), Total = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Notes = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true), Notes = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
InvoiceUrl = table.Column<string>(type: "text", nullable: true) InvoiceUrl = table.Column<string>(type: "text", nullable: true)
}, },
@@ -92,10 +91,10 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false), Summary = table.Column<string>(type: "character varying(512)", maxLength: 512, nullable: false),
Description = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), Description = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ImageUrl = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true), ImageUrl = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: true),
ThumbnailUrls = table.Column<string>(type: "jsonb", nullable: true), ThumbnailUrls = table.Column<string[]>(type: "text[]", nullable: true),
Categories = table.Column<string>(type: "jsonb", nullable: true), Categories = table.Column<string[]>(type: "text[]", nullable: true),
Metadata = table.Column<ProductMetadata>(type: "jsonb", nullable: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false) Metadata = table.Column<string>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -260,30 +259,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.Restrict);
}); });
migrationBuilder.CreateTable(
name: "ProductPrice",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ProductId = table.Column<long>(type: "bigint", nullable: false),
Amount = table.Column<decimal>(type: "numeric", nullable: false),
Discount = table.Column<decimal>(type: "numeric", nullable: false),
Enabled = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProductPrice", x => x.Id);
table.ForeignKey(
name: "FK_ProductPrice_Products_ProductId",
column: x => x.ProductId,
principalTable: "Products",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Shippings", name: "Shippings",
columns: table => new columns: table => new
@@ -334,9 +309,9 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
ContentType = 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), Number = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
Content = table.Column<byte[]>(type: "bytea", nullable: false), Content = table.Column<byte[]>(type: "bytea", nullable: false),
Notes = table.Column<string>(type: "jsonb", nullable: true), Notes = table.Column<string[]>(type: "text[]", nullable: true),
References = table.Column<PageReference[]>(type: "jsonb", nullable: true), Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
Enabled = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true) References = table.Column<string>(type: "jsonb", nullable: true)
}, },
constraints: table => constraints: table =>
{ {
@@ -428,12 +403,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
table: "Prices", table: "Prices",
column: "ProductId"); column: "ProductId");
migrationBuilder.CreateIndex(
name: "IX_ProductPrice_ProductId",
table: "ProductPrice",
column: "ProductId",
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Refunds_OrderId", name: "IX_Refunds_OrderId",
table: "Refunds", table: "Refunds",
@@ -468,9 +437,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "OrderItems"); name: "OrderItems");
migrationBuilder.DropTable(
name: "ProductPrice");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Refunds"); name: "Refunds");
@@ -1,7 +1,6 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using LiteCharms.Features.MidrandBooks.Postgres; using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.Models;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -109,9 +108,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Property<int>("PublisherType") b.Property<int>("PublisherType")
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<string>("ThumbnailImageUrl") b.Property<string>("ThumbnailImageUrl")
.HasMaxLength(2048) .HasMaxLength(2048)
.HasColumnType("character varying(2048)"); .HasColumnType("character varying(2048)");
@@ -288,9 +284,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.IsRequired() .IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<SocialMedia[]>("SocialMedia")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
@@ -335,7 +328,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("integer"); .HasColumnType("integer");
b.Property<decimal>("Total") b.Property<decimal>("Total")
.HasColumnType("decimal(18,2)"); .HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime?>("UpdatedAt") b.Property<DateTime?>("UpdatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -491,17 +485,14 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasColumnType("boolean") .HasColumnType("boolean")
.HasDefaultValue(true); .HasDefaultValue(true);
b.PrimitiveCollection<string>("Notes") b.PrimitiveCollection<string[]>("Notes")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<int>("Number") b.Property<int>("Number")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
.HasColumnType("integer") .HasColumnType("integer")
.HasDefaultValue(0); .HasDefaultValue(0);
b.Property<PageReference[]>("References")
.HasColumnType("jsonb");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -567,8 +558,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id")); NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.PrimitiveCollection<string>("Categories") b.PrimitiveCollection<string[]>("Categories")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd() .ValueGeneratedOnAdd()
@@ -588,9 +579,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(1024) .HasMaxLength(1024)
.HasColumnType("character varying(1024)"); .HasColumnType("character varying(1024)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name") b.Property<string>("Name")
.IsRequired() .IsRequired()
.HasMaxLength(255) .HasMaxLength(255)
@@ -601,8 +589,8 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.HasMaxLength(512) .HasMaxLength(512)
.HasColumnType("character varying(512)"); .HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("ThumbnailUrls") b.PrimitiveCollection<string[]>("ThumbnailUrls")
.HasColumnType("jsonb"); .HasColumnType("text[]");
b.Property<int>("Type") b.Property<int>("Type")
.HasColumnType("integer"); .HasColumnType("integer");
@@ -658,40 +646,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.ToTable("Prices", (string)null); b.ToTable("Prices", (string)null);
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
b.Property<decimal>("Amount")
.HasColumnType("numeric");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<decimal>("Discount")
.HasColumnType("numeric");
b.Property<bool>("Enabled")
.HasColumnType("boolean");
b.Property<long>("ProductId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId")
.IsUnique();
b.ToTable("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author") b.HasOne("LiteCharms.Features.MidrandBooks.Authors.Entities.Author", "Author")
@@ -711,6 +665,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Product"); 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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Customers.Entities.Address", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer") b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
@@ -733,6 +719,38 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Customer"); 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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Orders.Entities.OrderItem", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook") b.HasOne("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", "AuthorBook")
@@ -795,7 +813,34 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
.OnDelete(DeleteBehavior.NoAction) .OnDelete(DeleteBehavior.NoAction)
.IsRequired(); .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("Book");
b.Navigation("References");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
@@ -809,6 +854,35 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Order"); 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 => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
{ {
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product") b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
@@ -820,15 +894,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
b.Navigation("Product"); b.Navigation("Product");
}); });
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", null)
.WithOne("Price")
.HasForeignKey("LiteCharms.Features.MidrandBooks.Products.Models.ProductPrice", "ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.AuthorBooks.Entities.AuthorBook", b =>
{ {
b.Navigation("Pages"); b.Navigation("Pages");
@@ -862,8 +927,6 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b => modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.Product", b =>
{ {
b.Navigation("Price");
b.Navigation("Prices"); b.Navigation("Prices");
}); });
#pragma warning restore 612, 618 #pragma warning restore 612, 618
@@ -4,6 +4,8 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
{ {
public void Configure(EntityTypeBuilder<Product> builder) public void Configure(EntityTypeBuilder<Product> builder)
{ {
builder.Ignore(p => p.Price);
builder.ToTable("Products"); builder.ToTable("Products");
builder.HasKey(f => f.Id); builder.HasKey(f => f.Id);
@@ -14,9 +16,10 @@ public sealed class ProductConfiguration : IEntityTypeConfiguration<Product>
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512); builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
builder.Property(f => f.Description).HasMaxLength(1024); builder.Property(f => f.Description).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).HasMaxLength(1024); builder.Property(f => f.ImageUrl).HasMaxLength(1024);
builder.Property(f => f.ThumbnailUrls).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Metadata).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Categories).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Enabled).HasDefaultValue(false); builder.Property(f => f.Enabled).HasDefaultValue(false);
builder.Property(f => f.Categories).IsRequired(false);
builder.Property(f => f.ThumbnailUrls).IsRequired(false);
builder.OwnsOne(f => f.Metadata, b => { b.ToJson(); });
} }
} }
@@ -3,5 +3,5 @@
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>] [EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
public class ProductPrice : Models.ProductPrice public class ProductPrice : Models.ProductPrice
{ {
public virtual Product Product { get; set; } = new(); public virtual Product? Product { get; set; }
} }
@@ -1,10 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public sealed class CreateProductPrice
{
public long ProductId { get; set; }
public decimal Amount { get; set; }
public decimal Discount { get; set; }
}
@@ -20,3 +20,10 @@ public sealed record CreateProduct
public ProductMetadata? Metadata { get; set; } public ProductMetadata? Metadata { get; set; }
} }
public sealed class CreateProductPrice
{
public decimal Amount { get; set; }
public decimal Discount { get; set; }
}
@@ -14,17 +14,15 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var productPrice = await context.Prices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken); var rowsUpdated = await context.Prices
.Where(p => p.Id == productPriceId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Enabled, isEnabled)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (productPrice is null) return rowsUpdated > 0
return Result.Fail(new Error($"Product price with ID {productPriceId} not found"));
productPrice.UpdatedAt = DateTime.UtcNow;
productPrice.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error($"Failed to change status of product price with ID {productPriceId}")); : Result.Fail(new Error($"Product price with ID {productPriceId} not found"));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -38,17 +36,15 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{ {
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken); var rowsUpdated = await context.Products
.Where(p => p.Id == productId)
.ExecuteUpdateAsync(setters => setters
.SetProperty(p => p.Enabled, isEnabled)
.SetProperty(p => p.UpdatedAt, DateTime.UtcNow), cancellationToken);
if (product is null) return rowsUpdated > 0
return Result.Fail(new Error($"Product with ID {productId} not found"));
product.UpdatedAt = DateTime.UtcNow;
product.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok() ? Result.Ok()
: Result.Fail(new Error($"Failed to change status of product with ID {productId}")); : Result.Fail(new Error($"Product with ID {productId} not found"));
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -64,17 +60,22 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
var query = context.Products.AsQueryable(); 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)) if (!string.IsNullOrWhiteSpace(filter.Title))
query = query.Where(p => p.Name!.Contains(filter.Title)); query = query.Where(p => EF.Functions.ILike(p.Name!, $"%{filter.Title}%"));
if (!string.IsNullOrWhiteSpace(filter.Category)) if (!string.IsNullOrWhiteSpace(filter.Category))
query = query.Where(p => p.Categories!.Any(c => c == filter.Category)); query = query.Where(p => p.Categories.Contains(filter.Category));
if (!string.IsNullOrWhiteSpace(filter.Manufacturer)) if (!string.IsNullOrWhiteSpace(filter.Manufacturer))
query = query.Where(p => p.Metadata!.Manufacturer == filter.Manufacturer); query = query.Where(p => EF.Functions.ILike(p.Metadata!.Manufacturer!, $"%{filter.Manufacturer}%"));
if (!string.IsNullOrWhiteSpace(filter.SerialNumber)) if (!string.IsNullOrWhiteSpace(filter.SerialNumber))
query = query.Where(p => p.Metadata!.SerialNumber == filter.SerialNumber); query = query.Where(p => EF.Functions.ILike(p.Metadata!.SerialNumber!, $"%{filter.SerialNumber}%"));
if (filter.MinPrice > 0) if (filter.MinPrice > 0)
query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice)); query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice));
@@ -165,7 +166,7 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
} }
} }
public async ValueTask<Result<ProductPrice[]>> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default) public async ValueTask<Result<ProductPrice>> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default)
{ {
try try
{ {
@@ -175,16 +176,16 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
.AsNoTracking() .AsNoTracking()
.OrderByDescending(p => p.CreatedAt) .OrderByDescending(p => p.CreatedAt)
.ThenBy(p => p.UpdatedAt) .ThenBy(p => p.UpdatedAt)
.FirstOrDefaultAsync(p => p.ProductId == productId, cancellationToken); .FirstOrDefaultAsync(p => p.ProductId == productId && p.Enabled, cancellationToken);
return product is not null return product is not null
? Result.Ok(new[] { product.ToModel() }) ? Result.Ok(product.ToModel())
: Result.Fail<ProductPrice[]>(new Error($"No price found for product with ID {productId}")); : Result.Fail<ProductPrice>(new Error($"No price found for product with ID {productId}"));
} }
catch (Exception ex) catch (Exception ex)
{ {
return Result.Fail<ProductPrice[]>(new Error(ex.Message).CausedBy(ex)); return Result.Fail<ProductPrice>(new Error(ex.Message).CausedBy(ex));
} }
} }
@@ -228,8 +229,8 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
{ {
try try
{ {
var fromDate = range.From.ToDateTime(TimeOnly.MinValue); var fromDate = range.From.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue); var toDate = range.To.ToDateTime(TimeOnly.MaxValue, DateTimeKind.Utc);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken); await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
@@ -244,7 +245,7 @@ public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> cont
.AsSplitQuery() .AsSplitQuery()
.ToArrayAsync(cancellationToken); .ToArrayAsync(cancellationToken);
return await context.SaveChangesAsync(cancellationToken) > 0 return products?.Length > 0
? Result.Ok(products.Select(p => p.ToModel()).ToArray()) ? Result.Ok(products.Select(p => p.ToModel()).ToArray())
: Result.Fail<Product[]>(new Error("Failed to retrieve products.")); : Result.Fail<Product[]>(new Error("Failed to retrieve products."));
} }
+2 -1
View File
@@ -93,7 +93,8 @@ public enum SocialMediaTypes : int
YouTube = 5, YouTube = 5,
Pinterest = 6, Pinterest = 6,
Reddit = 7, Reddit = 7,
Tumblr = 8 Tumblr = 8,
GitHub = 9
} }
public enum EmailStatuses : int public enum EmailStatuses : int
+1
View File
@@ -9,6 +9,7 @@
<Project Path="LiteCharms.Features/LiteCharms.Features.csproj" /> <Project Path="LiteCharms.Features/LiteCharms.Features.csproj" />
</Folder> </Folder>
<Folder Name="/tests/"> <Folder Name="/tests/">
<Project Path="LiteCharms.Features.MidrandBooks.Seed/LiteCharms.Features.MidrandBooks.Seed.csproj" Id="aa80643a-28dc-431f-b163-053a94e5c77c" />
<Project Path="LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj" Id="cac2f738-dbb5-4538-8565-3c2bd6f65259" /> <Project Path="LiteCharms.Features.MidrandBooks.Tests/LiteCharms.Features.MidrandBooks.Tests.csproj" Id="cac2f738-dbb5-4538-8565-3c2bd6f65259" />
<Project Path="LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj" Id="0e0967c2-7f28-4668-a387-2fc437ab066f" /> <Project Path="LiteCharms.Features.TechShop.Tests/LiteCharms.Features.TechShop.Tests.csproj" Id="0e0967c2-7f28-4668-a387-2fc437ab066f" />
<Project Path="LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj" Id="0696323f-7148-4ab9-9145-68b7b5df5415" /> <Project Path="LiteCharms.Features.Tests/LiteCharms.Features.Tests.csproj" Id="0696323f-7148-4ab9-9145-68b7b5df5415" />