Compare commits

..
18 Commits
Author SHA1 Message Date
khwezi 47111a1a3a Merge pull request 'emailjobs' (#18) from emailjobs into master
Reviewed-on: #18
2026-05-14 02:49:34 +02:00
khwezi 6eb3d50375 Merge pull request 'Updated job scheduler' (#17) from emailjobs into master
Reviewed-on: #17
2026-05-10 17:33:23 +02:00
khwezi 4deb732804 Merge pull request 'emailjobs' (#16) from emailjobs into master
Reviewed-on: #16
2026-05-10 16:51:24 +02:00
khwezi 20d9387d0b Merge pull request 'Migrated database changes' (#15) from develop into master
Reviewed-on: #15
2026-05-10 11:17:54 +02:00
khwezi 9f6d0ccaa0 Merge pull request 'Populated README' (#14) from develop into master
Reviewed-on: #14
2026-05-10 09:48:35 +02:00
khwezi 1acbc4d213 Merge pull request 'fixed git repo naming' (#13) from develop into master
Reviewed-on: #13
2026-05-10 09:22:37 +02:00
khwezi 8c99668fac Merge pull request 'Added tag and release step after publish' (#12) from develop into master
Reviewed-on: #12
2026-05-10 09:19:33 +02:00
khwezi ad44f46204 Merge pull request 'Refactored Quartz instance id to AUTO, removed constant' (#11) from develop into master
Reviewed-on: #11
2026-05-10 08:21:08 +02:00
khwezi 49d999c1e3 Merge pull request 'Refactored postgres extension' (#10) from develop into master
Reviewed-on: #10
2026-05-09 17:45:00 +02:00
khwezi 9ed4777a18 Merge pull request 'Refactored database references' (#9) from develop into master
Reviewed-on: #9
2026-05-09 17:01:02 +02:00
khwezi 0cf44f68cc Merge pull request 'Added scheduler constants' (#8) from develop into master
Reviewed-on: #8
2026-05-09 15:27:50 +02:00
khwezi 41ed5a4288 Merge pull request 'Fixed quartz host config table prefix' (#7) from develop into master
Reviewed-on: #7
2026-05-09 13:30:33 +02:00
khwezi bbcba5e06c Merge pull request 'Fixed quartz table name prefix' (#6) from develop into master
Reviewed-on: #6
2026-05-09 13:28:29 +02:00
khwezi 502cc326dd Merge pull request 'Updated pipeline to use major version with minor always 0' (#5) from develop into master
Reviewed-on: #5
2026-05-09 12:02:19 +02:00
khwezi 4675d4c5fc Merge pull request 'Updated pipeline to use major versions only' (#4) from develop into master
continuous-integration/drone/push Build was killed
continuous-integration/drone Build is failing
Reviewed-on: #4
2026-05-09 11:56:20 +02:00
khwezi f80bb2fff9 Merge pull request 'Changed target branch from nain to master' (#3) from dronepipeline into master
continuous-integration/drone/push Build is passing
continuous-integration/drone Build is passing
Reviewed-on: #3
2026-05-09 11:37:39 +02:00
khwezi 6767906b0d Merge pull request 'Added pipeline separator' (#2) from dronepipeline into master
Reviewed-on: #2
2026-05-09 11:25:34 +02:00
khwezi a344af4498 Merge pull request 'Added .drone.yml pipeline' (#1) from dronepipeline into master
Reviewed-on: #1
2026-05-09 11:12:38 +02:00
170 changed files with 557 additions and 5757 deletions
+1 -6
View File
@@ -19,10 +19,6 @@ steps:
commands:
- dotnet pack LiteCharms.Features/LiteCharms.Features.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- dotnet nuget push dist/LiteCharms.Features.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
- dotnet pack LiteCharms.Features.TechShop/LiteCharms.Features.TechShop.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- dotnet nuget push dist/LiteCharms.Features.TechShop.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
- dotnet pack LiteCharms.Features.MidrandBooks/LiteCharms.Features.MidrandBooks.csproj -c Release -p:PackageVersion=$VERSION -o dist/
- dotnet nuget push dist/LiteCharms.Features.MidrandBooks.$VERSION.nupkg --api-key $NEXUS_KEY --source $NEXUS_URL
- name: gitea-tag-release
image: alpine/git
@@ -32,7 +28,6 @@ steps:
GITEA_PASS: { from_secret: git_password }
VERSION: 1.${DRONE_BUILD_NUMBER}.0
commands:
- echo "169.255.58.144 gitea.khongisa.co.za" >> /etc/hosts
- apk add --no-cache curl
- git remote set-url origin https://$${GITEA_USER}:$${GITEA_PASS}@gitea.khongisa.co.za/litecharms/components.git
- git tag $VERSION
@@ -45,7 +40,7 @@ steps:
\"tag_name\": \"$VERSION\",
\"target_commitish\": \"${DRONE_COMMIT_SHA}\",
\"name\": \"Library Suite $VERSION\",
\"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Features\n* LiteCharms.Features.TechShop\n* LiteCharms.Features.MidrandBooks\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
\"body\": \"### Published NuGet Packages\nAll packages versioned as **$VERSION**:\n* LiteCharms.Abstractions\n* LiteCharms.Features\n\n[View in Nexus](https://nexus.khongisa.co.za/repository/nuget-group/)\",
\"draft\": false,
\"prerelease\": false
}"
@@ -1,38 +0,0 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.MidrandBooks.Extensions;
namespace LiteCharms.Features.MidrandBooks.Tests;
public class Fixture : IDisposable
{
public IConfiguration Configuration { get; set; }
public IServiceProvider Services { get; set; }
public IMediator Mediator { get; set; }
public Fixture()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets<Fixture>()
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
Services = new ServiceCollection()
.AddMediator()
.AddLogging()
//.AddMidrandShopServices()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddMidrandShopDatabase(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
}
public void Dispose() { }
}
@@ -1,51 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>b205af96-ceef-44e1-851c-458c9fd1c437</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<!-- Global Usings -->
<ItemGroup>
<Using Include="Mediator" />
<Using Include="Xunit.Abstractions" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Features.MidrandBooks\LiteCharms.Features.MidrandBooks.csproj" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -1,34 +0,0 @@
{
"BookshopS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop",
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
},
"BookshopQuotesS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop.quotes",
"CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za"
},
"Email": {
"Credentials": {
"Username": "shop@litecharms.co.za"
},
"Port": 465,
"Host": "mail.litecharms.co.za",
"UseSsl": true
},
"Monitoring": {
"ApiKey": "",
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
"ServiceName": "LiteCharms.LeadGenerator"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
@@ -1,3 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Abstractions;
public interface IService;
@@ -1,141 +0,0 @@
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres;
namespace LiteCharms.Features.MidrandBooks.AuthorBooks;
public class BooksService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result> UpdateBookStatusAsync(long bookId, bool isEnabled, CancellationToken cancellationToken)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var book = await context.Books.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken);
if (book is null)
return Result.Fail(new Error($"Book with ID {bookId} not found"));
book.UpdatedAt = DateTime.UtcNow;
book.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to change status of book with ID {bookId}"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateBookAsync(long authorId, long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken))
return Result.Fail<long>("Author not found.");
if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken))
return Result.Fail<long>("Product not found.");
var book = context.Books.Add(new Entities.AuthorBook
{
AuthorId = authorId,
ProductId = productId,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(book.Entity.Id)
: Result.Fail<long>("Failed to create book.");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<AuthorBook>> GetBookAsync(long bookId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var book = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.Include(b => b.Pages)
.FirstOrDefaultAsync(b => b.Id == bookId, cancellationToken);
return book is null
? Result.Fail<AuthorBook>(new Error($"Book with ID {bookId} not found"))
: Result.Ok(book.ToModel());
}
catch (Exception ex)
{
return Result.Fail<AuthorBook>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<AuthorBook[]>> GetBooksByAuthorAsync(long authorId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if(!await context.Authors.AnyAsync(a => a.Id == authorId, cancellationToken))
return Result.Fail<AuthorBook[]>(new Error($"Author with ID {authorId} not found"));
var books = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.OrderByDescending(b => b.CreatedAt)
.Where(b => b.AuthorId == authorId)
.ToListAsync(cancellationToken);
return books?.Count > 0
? Result.Ok(books.Select(b => b.ToModel()).ToArray())
: Result.Fail<AuthorBook[]>(new Error($"No books found for author with ID {authorId}"));
}
catch (Exception ex)
{
return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<AuthorBook[]>> GetPublishedBooksAsync(int offset, int limit, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var books = await context.Books
.AsNoTracking()
.Include(b => b.Author)
.Include(b => b.Product!.Price)
.Include(b => b.Pages)
.Where(b => b.Enabled && b.Product!.Enabled && b.Author.Enabled)
.OrderByDescending(b => b.Ranking)
.ThenByDescending(b => b.Ranking)
.ThenByDescending(b => b.CreatedAt)
.ThenByDescending(b => b.UpdatedAt)
.Skip(offset).Take(limit)
.AsSplitQuery()
.ToArrayAsync(cancellationToken);
return books?.Length > 0
? Result.Ok(books.Select(b => b.ToModel()).ToArray())
: Result.Fail<AuthorBook[]>(new Error("No published books found."));
}
catch (Exception ex)
{
return Result.Fail<AuthorBook[]>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -1,14 +0,0 @@
using LiteCharms.Features.MidrandBooks.Authors.Entities;
using LiteCharms.Features.MidrandBooks.Pages.Entities;
using LiteCharms.Features.MidrandBooks.Products.Entities;
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
public class AuthorBook : Models.AuthorBook
{
public virtual Author Author { get; set; } = new();
public new virtual Product? Product { get; set; }
public virtual ICollection<BookPage> Pages { get; set; } = [];
}
@@ -1,28 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
public class AuthorBookConfiguration : IEntityTypeConfiguration<AuthorBook>
{
public void Configure(EntityTypeBuilder<AuthorBook> builder)
{
builder.ToTable("Books");
builder.HasKey(f => f.AuthorId);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(f => f.AuthorId).IsRequired();
builder.Property(f => f.ProductId).IsRequired();
builder.Property(f => f.Rating).IsRequired(false);
builder.Property(f => f.Ranking).IsRequired(false);
builder.Property(f => f.Enabled).HasDefaultValue(true);
builder.HasOne(f => f.Author)
.WithMany(a => a.Books)
.HasForeignKey(f => f.AuthorId)
.OnDelete(DeleteBehavior.Restrict);
builder.HasOne(f => f.Product)
.WithMany()
.HasForeignKey(f => f.ProductId)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -1,24 +0,0 @@
using LiteCharms.Features.MidrandBooks.Products.Models;
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
public class AuthorBook
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public long AuthorId { get; set; }
public long ProductId { get; set; }
public int Rating { get; set; }
public int Ranking { get; set; }
public Product? Product { get; set; }
public bool Enabled { get; set; }
}
@@ -1,186 +0,0 @@
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Authors;
public class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result<AuthorBook[]>> GetAuthorBooksAsync(long authorId, 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<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.Fail(new Error($"Failed to change status of author with ID {authorId}"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Author>> GetAuthorAsync(long authorId, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
return author is not null
? Result.Ok(author.ToModel())
: Result.Fail<Author>(new Error($"Author with ID {authorId} not found"));
}
catch (Exception ex)
{
return Result.Fail<Author>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Author[]>> GetAuthors(DateRange range, CancellationToken cancellationToken)
{
try
{
var fromDate = range.From.ToDateTime(TimeOnly.MinValue);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var authors = await context.Authors.AsNoTracking()
.OrderByDescending(o => o.CreatedAt)
.ThenByDescending(o => o.UpdatedAt)
.Where(a => a.CreatedAt >= fromDate && a.CreatedAt <= toDate)
.Take(range.MaxRecords)
.ToArrayAsync(cancellationToken);
return authors?.Length > 0
? Result.Ok(authors.Select(a => a.ToModel()).ToArray())
: Result.Fail<Author[]>(new Error("No authors found in the specified date range."));
}
catch (Exception ex)
{
return Result.Fail<Author[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateAuthorAsync(long authorId, UpdateAuthor request, CancellationToken cancellationToken)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken))
return Result.Fail(new Error($"An author with the name {request.Name} {request.LastName} already exists"));
if (await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken))
return Result.Fail(new Error($"An author with the email {request.Email} already exists"));
var author = await context.Authors.FirstOrDefaultAsync(a => a.Id == authorId, cancellationToken);
if (author is null)
return Result.Fail(new Error($"Author with ID {authorId} not found"));
author.UpdatedAt = DateTime.UtcNow;
author.PublisherType = request.PublisherType;
author.Company = request.Company;
author.VatNumber = request.VatNumber;
author.Name = request.Name;
author.LastName = request.LastName;
author.Biography = request.Biography;
author.Email = request.Email;
author.Website = request.Website;
author.ImageUrl = request.ImageUrl;
author.ThumbnailImageUrl = request.ThumbnailImageUrl;
author.SocialMedia = request.SocialMedia;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to update author with ID {authorId}"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateAuthorAsync(CreateAuthor request, CancellationToken cancellationToken = default)
{
try
{
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if(await context.Authors.AnyAsync(a => a.Name == request.Name && a.LastName == request.LastName, cancellationToken))
return Result.Fail<long>(new Error($"An author with the name {request.Name} {request.LastName} already exists"));
if(await context.Authors.AnyAsync(a => a.Email == request.Email, cancellationToken))
return Result.Fail<long>(new Error($"An author with the email {request.Email} already exists"));
var newAuthor = context.Authors.Add(new Entities.Author
{
Company = request.Company,
VatNumber = request.VatNumber,
PublisherType = request.PublisherType,
Name = request.Name,
LastName = request.LastName,
Biography = request.Biography,
Email = request.Email,
Website = request.Website,
ImageUrl = request.ImageUrl,
ThumbnailImageUrl = request.ThumbnailImageUrl,
SocialMedia = request.SocialMedia
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(newAuthor.Entity.Id)
: Result.Fail<long>(new Error($"Failed to create author {request.Name} {request.LastName}"));
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -1,9 +0,0 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
namespace LiteCharms.Features.MidrandBooks.Authors.Entities;
[EntityTypeConfiguration<AuthorConfiguration, Author>]
public class Author : Models.Author
{
public ICollection<AuthorBook> Books { get; set; } = [];
}
@@ -1,24 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Authors.Entities;
public class AuthorConfiguration : IEntityTypeConfiguration<Author>
{
public void Configure(EntityTypeBuilder<Author> builder)
{
builder.ToTable("Authors");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(f => f.PublisherType).IsRequired();
builder.Property(f => f.VatNumber).IsRequired(false).HasMaxLength(255);
builder.Property(f => f.Name).IsRequired().HasMaxLength(255);
builder.Property(f => f.LastName).IsRequired().HasMaxLength(255);
builder.Property(f => f.Biography).IsRequired(false).HasMaxLength(2048);
builder.Property(f => f.Email).IsRequired().HasMaxLength(512);
builder.Property(f => f.Website).IsRequired(false).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).IsRequired().HasMaxLength(2048);
builder.Property(f => f.ThumbnailImageUrl).IsRequired(false).HasMaxLength(2048);
builder.Property(f => f.SocialMedia).IsRequired(false).HasColumnType("jsonb");
builder.Property(f => f.Enabled).HasDefaultValue(true);
}
}
@@ -1,36 +0,0 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Authors.Models;
public class Author
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public PublisherTypes PublisherType { get; set; }
public string? Company { get; set; }
public string? VatNumber { get; set; }
public string? Name { get; set; }
public string? LastName { get; set; }
public string? Biography { get; set; }
public string? Email { get; set; }
public string? Website { get; set; }
public string? ImageUrl { get; set; }
public string? ThumbnailImageUrl { get; set; }
public SocialMedia[]? SocialMedia { get; set; }
public bool Enabled { get; set; }
}
@@ -1,30 +0,0 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Authors.Models;
public record UpdateAuthor : CreateAuthor;
public record CreateAuthor
{
public required PublisherTypes PublisherType { get; set; }
public string? Company { get; set; }
public string? VatNumber { get; set; }
public required string Name { get; set; }
public required string LastName { get; set; }
public string? Biography { get; set; }
public required string Email { get; set; }
public string? Website { get; set; }
public required string ImageUrl { get; set; }
public string? ThumbnailImageUrl { get; set; }
public SocialMedia[]? SocialMedia { get; set; }
}
@@ -1,408 +0,0 @@
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres;
namespace LiteCharms.Features.MidrandBooks.Customers;
public class CustomerService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result<long>> CreateCustomerAsync(CreateCustomer request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Customers.AnyAsync(c => c.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken))
return Result.Fail<long>(new Error($"Customer with email '{request.Email}' already exists."));
var customer = context.Customers.Add(new Entities.Customer
{
Company = request.Company,
VatNumber = request.VatNumber,
Email = request.Email,
Website = request.Website,
Phone = request.Phone,
SocialMedia = request.SocialMedia,
Enabled = true
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(customer.Entity.Id)
: Result.Fail<long>(new Error("Failed to create customer."));
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateCustomerContactAsync(long customerId, CreateCustomerContact request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
return Result.Fail<long>(new Error($"Customer with ID '{customerId}' does not exist."));
if (await context.Contacts.AnyAsync(cc => cc.CustomerId == customerId && cc.Email!.Equals(request.Email, StringComparison.OrdinalIgnoreCase), cancellationToken))
return Result.Fail<long>(new Error($"Contact with email '{request.Email}' already exists for this customer."));
var contact = context.Contacts.Add(new Entities.Contact
{
CustomerId = customerId,
Name = request.Name,
Email = request.Email,
Phone = request.Phone,
LastName = request.LastName,
Type = request.Type,
Enabled = true
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(contact.Entity.Id)
: Result.Fail<long>(new Error("Failed to create customer contact."));
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateCustomerAddressAsync(long customerId, CreateCustomerAddress request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
return Result.Fail<long>(new Error($"Customer with ID '{customerId}' does not exist."));
var address = context.Addresses.Add(new Entities.Address
{
CustomerId = customerId,
Street = request.Street,
City = request.City,
State = request.State,
PostalCode = request.PostalCode,
Country = request.Country,
Type = request.Type,
Enabled = true,
BuildingType = request.BuildingType,
IsPrimary = request.IsPrimary,
Name = request.Name
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(address.Entity.Id)
: Result.Fail<long>(new Error("Failed to create customer address."));
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateCustomerAsync(long customerId, UpdateCustomer request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken);
if (customer is null)
return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist."));
customer.UpdatedAt = DateTime.UtcNow;
customer.Company = request.Company;
customer.VatNumber = request.VatNumber;
customer.Email = request.Email;
customer.Website = request.Website;
customer.Phone = request.Phone;
customer.SocialMedia = request.SocialMedia;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateCustomerContactAsync(long contactId, UpdateCustomerContact request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken);
if (contact is null)
return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
contact.UpdatedAt = DateTime.UtcNow;
contact.Name = request.Name;
contact.LastName = request.LastName;
contact.Email = request.Email;
contact.Phone = request.Phone;
contact.Type = request.Type;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer contact."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateCustomerAddressAsync(long addressId, UpdateCustomerAddress request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken);
if (address is null)
return Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
address.UpdatedAt = DateTime.UtcNow;
address.Street = request.Street;
address.City = request.City;
address.State = request.State;
address.PostalCode = request.PostalCode;
address.Country = request.Country;
address.Type = request.Type;
address.BuildingType = request.BuildingType;
address.IsPrimary = request.IsPrimary;
address.Name = request.Name;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer address."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateCustomerStatusAsync(long customerId, bool enabled, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var customer = await context.Customers.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken);
if (customer is null)
return Result.Fail(new Error($"Customer with ID '{customerId}' does not exist."));
customer.Enabled = enabled;
customer.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer status."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateCustomerContactStatusAsync(long contactId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contact = await context.Contacts.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken);
if (contact is null)
return Result.Fail(new Error($"Contact with ID '{contactId}' does not exist."));
contact.Enabled = enabled;
contact.IsPrimary = isPrimary;
contact.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer contact status."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateCustomerAddressStatusAsync(long addressId, bool enabled, bool isPrimary, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var address = await context.Addresses.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken);
if (address is null)
return Result.Fail(new Error($"Address with ID '{addressId}' does not exist."));
address.Enabled = enabled;
address.IsPrimary = isPrimary;
address.UpdatedAt = DateTime.UtcNow;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error("Failed to update customer address status."));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Customer[]>> GetCustomersAsync(CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var customers = await context.Customers
.AsNoTracking()
.Include(c => c.Contacts)
.Include(c => c.Addresses)
.OrderByDescending(c => c.CreatedAt)
.ThenByDescending(c => c.UpdatedAt)
.AsSplitQuery()
.ToListAsync(cancellationToken);
return customers?.Count > 0
? Result.Ok(customers.Select(c => c.ToModel()).ToArray())
: Result.Fail<Customer[]>(new Error("No customers found."));
}
catch (Exception ex)
{
return Result.Fail<Customer[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Contact[]>> GetCustomerContactsAsync(long customerId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
return Result.Fail<Contact[]>(new Error($"Customer with ID '{customerId}' does not exist."));
var contacts = await context.Contacts
.AsNoTracking()
.Where(cc => cc.CustomerId == customerId)
.OrderByDescending(cc => cc.CreatedAt)
.ThenByDescending(cc => cc.UpdatedAt)
.ToListAsync(cancellationToken);
return contacts?.Count > 0
? Result.Ok(contacts.Select(cc => cc.ToModel()).ToArray())
: Result.Fail<Contact[]>(new Error("No contacts found for the specified customer."));
}
catch (Exception ex)
{
return Result.Fail<Contact[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Address[]>> GetCustomerAddressesAsync(long customerId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Customers.AnyAsync(c => c.Id == customerId, cancellationToken))
return Result.Fail<Address[]>(new Error($"Customer with ID '{customerId}' does not exist."));
var addresses = await context.Addresses
.AsNoTracking()
.Where(a => a.CustomerId == customerId)
.OrderByDescending(a => a.CreatedAt)
.ThenByDescending(a => a.UpdatedAt)
.ToListAsync(cancellationToken);
return addresses?.Count > 0
? Result.Ok(addresses.Select(a => a.ToModel()).ToArray())
: Result.Fail<Address[]>(new Error($"No addresses found for customer with ID '{customerId}'."));
}
catch (Exception ex)
{
return Result.Fail<Address[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Customer>> GetCustomerAsync(long customerId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var customer = await context.Customers
.AsNoTracking()
.Include(c => c.Contacts)
.Include(c => c.Addresses)
.FirstOrDefaultAsync(c => c.Id == customerId, cancellationToken);
return customer is not null
? Result.Ok(customer.ToModel())
: Result.Fail<Customer>(new Error($"Customer with ID '{customerId}' does not exist."));
}
catch (Exception ex)
{
return Result.Fail<Customer>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Contact>> GetCustomerContactAsync(long contactId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var contact = await context.Contacts
.AsNoTracking()
.FirstOrDefaultAsync(cc => cc.Id == contactId, cancellationToken);
return contact is not null
? Result.Ok(contact.ToModel())
: Result.Fail<Contact>(new Error($"Contact with ID '{contactId}' does not exist."));
}
catch (Exception ex)
{
return Result.Fail<Contact>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Address>> GetCustomerAddressAsync(long addressId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var address = await context.Addresses
.AsNoTracking()
.FirstOrDefaultAsync(a => a.Id == addressId, cancellationToken);
return address is not null
? Result.Ok(address.ToModel())
: Result.Fail<Address>(new Error($"Address with ID '{addressId}' does not exist."));
}
catch (Exception ex)
{
return Result.Fail<Address>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -1,7 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
[EntityTypeConfiguration<AddressConfiguration, Address>]
public class Address : Models.Address
{
public virtual Customer? Customer { get; set; }
}
@@ -1,29 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
public class AddressConfiguration : IEntityTypeConfiguration<Address>
{
public void Configure(EntityTypeBuilder<Address> builder)
{
builder.ToTable("Addresses");
builder.HasKey(a => a.Id);
builder.Property(a => a.CustomerId).IsRequired();
builder.Property(a => a.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(a => a.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(a => a.Name).IsRequired();
builder.Property(a => a.Type).IsRequired();
builder.Property(a => a.BuildingType).IsRequired();
builder.Property(a => a.Street).IsRequired();
builder.Property(a => a.City).IsRequired();
builder.Property(a => a.State).IsRequired();
builder.Property(a => a.PostalCode).IsRequired();
builder.Property(a => a.Country).IsRequired();
builder.Property(a => a.IsPrimary).HasDefaultValue(false);
builder.Property(a => a.Enabled).HasDefaultValue(true);
builder.HasOne(a => a.Customer)
.WithMany(c => c.Addresses)
.HasForeignKey(a => a.CustomerId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -1,7 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
[EntityTypeConfiguration<ContactConfiguration, Contact>]
public class Contact : Models.Contact
{
public virtual Customer? Customer { get; set; }
}
@@ -1,26 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
public class ContactConfiguration : IEntityTypeConfiguration<Contact>
{
public void Configure(EntityTypeBuilder<Contact> builder)
{
builder.ToTable("Contacts");
builder.HasKey(c => c.Id);
builder.Property(c => c.CustomerId).IsRequired();
builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(c => c.Name).IsRequired();
builder.Property(c => c.LastName).IsRequired();
builder.Property(c => c.Type).IsRequired();
builder.Property(c => c.Phone).IsRequired();
builder.Property(c => c.Email).IsRequired();
builder.Property(c => c.IsPrimary).HasDefaultValue(false);
builder.Property(c => c.Enabled).HasDefaultValue(true);
builder.HasOne(c => c.Customer)
.WithMany(c => c.Contacts)
.HasForeignKey(c => c.CustomerId)
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -1,9 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
[EntityTypeConfiguration<CustomerConfiguration, Customer>]
public class Customer : Models.Customer
{
public virtual ICollection<Contact> Contacts { get; set; } = [];
public virtual ICollection<Address> Addresses { get; set; } = [];
}
@@ -1,20 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.ToTable("Customers");
builder.HasKey(c => c.Id);
builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(c => c.Company).IsRequired(false);
builder.Property(c => c.VatNumber).IsRequired(false);
builder.Property(c => c.Email).IsRequired();
builder.Property(c => c.Phone).IsRequired();
builder.Property(c => c.Website).IsRequired();
builder.Property(c => c.SocialMedia).IsRequired(false).HasColumnType("jsonb");
builder.Property(c => c.Enabled).HasDefaultValue(true);
}
}
@@ -1,32 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
public class Address
{
public long Id { get; set; }
public long CustomerId { get; set; }
public string? Name { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public AddressType Type { get; set; }
public AddressBuildingTypes BuildingType { get; set; }
public string? Street { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? PostalCode { get; set; }
public string? Country { get; set; }
public bool IsPrimary { get; set; }
public bool Enabled { get; set; }
}
@@ -1,26 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
public class Contact
{
public long Id { get; set; }
public long CustomerId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public ContactTypes Type { get; set; }
public string? Name { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
public bool IsPrimary { get; set; }
public bool Enabled { get; set; }
}
@@ -1,26 +0,0 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
public class Customer
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? Company { get; set; }
public string? VatNumber { get; set; }
public string? Email { get; set; }
public string? Website { get; set; }
public string? Phone { get; set; }
public SocialMedia[]? SocialMedia { get; set; }
public bool Enabled { get; set; }
}
@@ -1,60 +0,0 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
public record CreateCustomer
{
public string? Company { get; set; }
public string? VatNumber { get; set; }
public string? Email { get; set; }
public string? Website { get; set; }
public string? Phone { get; set; }
public SocialMedia[]? SocialMedia { get; set; }
}
public record UpdateCustomer : CreateCustomer;
public record CreateCustomerContact
{
public ContactTypes Type { get; set; }
public string? Name { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public string? Phone { get; set; }
}
public record UpdateCustomerContact : CreateCustomerContact;
public record CreateCustomerAddress
{
public string? Name { get; set; }
public AddressType Type { get; set; }
public AddressBuildingTypes BuildingType { get; set; }
public string? Street { get; set; }
public string? City { get; set; }
public string? State { get; set; }
public string? PostalCode { get; set; }
public string? Country { get; set; }
public bool IsPrimary { get; set; }
public bool Enabled { get; set; }
}
public record UpdateCustomerAddress : CreateCustomerAddress;
-62
View File
@@ -1,62 +0,0 @@
namespace LiteCharms.Features.MidrandBooks;
public enum PublisherTypes : int
{
Individual = 0,
Company = 1,
Organization = 2,
SelfPublished = 3,
UniversityPress = 4,
GovernmentAgency = 5,
NonProfit = 6,
Independent = 7
}
public enum BookTypes : int
{
Fiction = 0,
NonFiction = 1,
Academic = 2,
SelfHelp = 3,
Biography = 4,
Poetry = 5,
Children = 6,
YoungAdult = 7,
ScienceFiction = 8,
Fantasy = 9
}
public enum BookContentTypes : int
{
Text = 0,
Image = 1,
Video = 2,
Audio = 3,
Interactive = 4,
Markdown = 5,
Html = 6,
Json = 7,
Yaml = 8
}
public enum BookPageTypes : int
{
Cover = 0,
Preface = 1,
Introduction = 2,
Content = 3,
Closing = 4,
Referencer = 5,
Credits = 6,
BackCover = 7
}
public enum ProductTypes : int
{
Book = 1,
Journal = 2,
Magazine = 3,
EBook = 4,
Audiobook = 5,
Accessory = 6
}
@@ -1,30 +0,0 @@
using LiteCharms.Features.MidrandBooks.HealthChecks;
using static LiteCharms.Features.Extensions.Postgres;
using static LiteCharms.Features.MidrandBooks.Extensions.Postgres;
namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class HealthChecks
{
public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<MidrandShopQuartzHealthCheck>(SchedulerDbConfigName);
return services;
}
public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandBooksDbConfigName);
return services;
}
public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration)
{
services.AddHealthChecks()
.AddCheck("Self", () => HealthCheckResult.Healthy());
return services;
}
}
@@ -1,133 +0,0 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
using LiteCharms.Features.MidrandBooks.Authors.Models;
using LiteCharms.Features.MidrandBooks.Customers.Models;
using LiteCharms.Features.MidrandBooks.Pages.Models;
using LiteCharms.Features.MidrandBooks.Products.Models;
namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class Mappers
{
public static Customer ToModel(this Customers.Entities.Customer entiry) => new()
{
Id = entiry.Id,
Company = entiry.Company,
CreatedAt = entiry.CreatedAt,
Email = entiry.Email,
Enabled = entiry.Enabled,
Phone = entiry.Phone,
SocialMedia = entiry.SocialMedia,
UpdatedAt = entiry.UpdatedAt,
VatNumber = entiry.VatNumber,
Website = entiry.Website
};
public static Address ToModel(this Customers.Entities.Address entity) => new()
{
Id = entity.Id,
BuildingType = entity.BuildingType,
CreatedAt = entity.CreatedAt,
CustomerId = entity.CustomerId,
Enabled = entity.Enabled,
IsPrimary = entity.IsPrimary,
Name = entity.Name,
PostalCode = entity.PostalCode,
Type = entity.Type,
UpdatedAt = entity.UpdatedAt,
Street = entity.Street,
City = entity.City,
State = entity.State,
Country = entity.Country
};
public static Contact ToModel(this Customers.Entities.Contact entity) => new()
{
Id = entity.Id,
Type = entity.Type,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
CustomerId = entity.CustomerId,
Email = entity.Email,
Enabled = entity.Enabled,
LastName = entity.LastName,
Name = entity.Name,
Phone = entity.Phone
};
public static BookPage ToModel(this Pages.Entities.BookPage entity) => new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
AuthorBookId = entity.AuthorBookId,
Content = entity.Content,
ContentType = entity.ContentType,
Number = entity.Number,
Enabled = entity.Enabled,
Notes = entity.Notes,
References = entity.References,
Type = entity.Type
};
public static AuthorBook ToModel(this AuthorBooks.Entities.AuthorBook entity) => new()
{
Id = entity.Id,
ProductId = entity.ProductId,
CreatedAt = entity.CreatedAt,
AuthorId = entity.AuthorId,
Ranking = entity.Ranking,
Rating = entity.Rating,
Enabled = entity.Enabled,
Product = entity.Product?.ToModel(),
};
public static ProductPrice ToModel(this Products.Entities.ProductPrice entity) => new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
ProductId = entity.ProductId,
Amount = entity.Amount,
Discount = entity.Discount,
Enabled = entity.Enabled
};
public static Product ToModel(this Products.Entities.Product entity)
{
return new Product
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
Name = entity.Name,
Summary = entity.Summary,
Description = entity.Description,
Type = entity.Type,
ImageUrl = entity.ImageUrl,
ThumbnailUrls = entity.ThumbnailUrls,
Metadata = entity.Metadata,
Categories = entity.Categories,
Enabled = entity.Enabled,
Price = entity.Prices?.FirstOrDefault(p => p.Enabled)?.ToModel() ?? null,
};
}
public static Author ToModel(this Authors.Entities.Author entity) => new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
UpdatedAt = entity.UpdatedAt,
Name = entity.Name,
LastName = entity.LastName,
Biography = entity.Biography,
Email = entity.Email,
Website = entity.Website,
ImageUrl = entity.ImageUrl,
ThumbnailImageUrl = entity.ThumbnailImageUrl,
SocialMedia = entity.SocialMedia,
Enabled = entity.Enabled,
Company = entity.Company,
PublisherType = entity.PublisherType,
VatNumber = entity.VatNumber
};
}
@@ -1,16 +0,0 @@
using LiteCharms.Features.MidrandBooks.Postgres;
namespace LiteCharms.Features.MidrandBooks.Extensions;
public static class Postgres
{
public const string MidrandBooksDbConfigName = "PostgresMidrandBooks";
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<MidrandBooksDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName)));
return services;
}
}
@@ -1,28 +0,0 @@
using static LiteCharms.Features.Extensions.Quartz;
namespace LiteCharms.Features.MidrandBooks.HealthChecks;
public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var scheduler = await schedulerFactory.GetScheduler(MidrandShopSchedulerName, cancellationToken);
if(scheduler == null)
return HealthCheckResult.Unhealthy($"Scheduler with name '{MidrandShopSchedulerName}' not found.");
if (!scheduler.IsStarted)
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler is not running");
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
return HealthCheckResult.Healthy($"{MidrandShopSchedulerName} Quartz scheduler is ready");
}
catch (SchedulerException)
{
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler cannot connect to the store");
}
}
}
@@ -1,28 +0,0 @@
using static LiteCharms.Features.MidrandBooks.Extensions.Postgres;
namespace LiteCharms.Features.MidrandBooks.HealthChecks;
public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck
{
private readonly string connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName)!;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
await using var command = connection.CreateCommand();
command.CommandText = "SELECT 1";
await command.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy($"{MidrandBooksDbConfigName} is responsive.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy($"{MidrandBooksDbConfigName} is unreachable.", ex);
}
}
}
@@ -1,170 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
<UserSecretsId>5be62f49-3ed0-4468-884e-1b04e048b45a</UserSecretsId>
</PropertyGroup>
<!-- Nuget Package Details -->
<PropertyGroup>
<PackageId>LiteCharms.Features.MidrandBooks</PackageId>
<Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company>
<Description>MidrandBooks feature components for Lite Charms applications.</Description>
<PackageProjectUrl>https://gitea.khongisa.co.za/litecharms/components</PackageProjectUrl>
<RepositoryUrl>https://gitea.khongisa.co.za/litecharms/components.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageTags>utility;dotnet</PackageTags>
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<!-- Quartz Scheduler-->
<ItemGroup>
<PackageReference Include="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="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>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Order\Entities\" />
</ItemGroup>
</Project>
@@ -1,22 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Order.Models;
public class Order
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public long CustomerId { get; set; }
public OrderStatus Status { get; set; }
public decimal Total { get; set; }
public string[]? Notes { get; set; }
public string[]? Terms { get; set; }
public string? InvoiceUrl { get; set; }
}
@@ -1,20 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Order.Models;
public class Refund
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public Guid OrderId { get; set; }
public RefundTypes Type { get; set; }
public RefundStatus Status { get; set; }
public string? Reason { get; set; }
public decimal Amount { get; set; }
}
@@ -1,14 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Order.Models;
public class Shipping
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public long OrderId { get; set; }
public ShippingStatuses Status { get; set; }
}
@@ -1,18 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Order.Models;
public class ShippingProvider
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public ShippingProviderTypes Type { get; set; }
public string? Name { get; set; }
public decimal? Price { get; set; }
public bool Enabled { get; set; }
}
@@ -1,9 +0,0 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
namespace LiteCharms.Features.MidrandBooks.Pages.Entities;
[EntityTypeConfiguration<BookPageConfiguration, BookPage>]
public class BookPage : Models.BookPage
{
public virtual AuthorBook Book { get; set; } = new();
}
@@ -1,26 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Pages.Entities;
public class BookPageConfiguration : IEntityTypeConfiguration<BookPage>
{
public void Configure(EntityTypeBuilder<BookPage> builder)
{
builder.ToTable("BookPages");
builder.HasKey(bp => bp.Id);
builder.Property(bp => bp.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(bp => bp.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(bp => bp.Number).IsRequired().HasDefaultValue(0);
builder.Property(bp => bp.AuthorBookId).IsRequired();
builder.Property(bp => bp.Content).IsRequired();
builder.Property(bp => bp.Type).IsRequired();
builder.Property(bp => bp.ContentType).IsRequired();
builder.Property(bp => bp.Notes).IsRequired(false).HasColumnType("jsonb");
builder.Property(bp => bp.References).IsRequired(false).HasColumnType("jsonb");
builder.Property(bp => bp.Enabled).HasDefaultValue(true);
builder.HasOne(f =>f.Book)
.WithMany(b => b.Pages)
.HasForeignKey(f => f.AuthorBookId)
.OnDelete(DeleteBehavior.NoAction);
}
}
@@ -1,28 +0,0 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
public class BookPage
{
public long Id { get; set; }
public long AuthorBookId { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public BookPageTypes Type { get; set; }
public BookContentTypes ContentType { get; set; }
public int Number { get; set; }
public byte[]? Content { get; set; }
public string[]? Notes { get; set; }
public PageReference[]? References { get; set; }
public bool Enabled { get; set; }
}
@@ -1,18 +0,0 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
public class CreateBookPage
{
public BookPageTypes Type { get; set; }
public BookContentTypes ContentType { get; set; }
public int Number { get; set; }
public byte[]? Content { get; set; }
public string[]? Notes { get; set; }
public PageReference[]? References { get; set; }
}
@@ -1,3 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
public class UpdateBookPage : CreateBookPage;
@@ -1,226 +0,0 @@
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Pages.Models;
using LiteCharms.Features.MidrandBooks.Postgres;
namespace LiteCharms.Features.MidrandBooks.Pages;
public class PageService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result> DeleteAllAsync(long authorBookId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken))
return Result.Fail("Book not found");
var pages = await context.Pages.Where(p => p.AuthorBookId == authorBookId).ToListAsync(cancellationToken);
if (pages.Count == 0)
return Result.Fail("No pages found for the specified book");
context.Pages.RemoveRange(pages);
await context.SaveChangesAsync(cancellationToken);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> DeleteByPageTypeAsync(long authorBookId, int pageNumber, BookPageTypes pageType, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.AuthorBookId == authorBookId && p.Number == pageNumber && p.Type == pageType, cancellationToken);
if (page is null)
return Result.Fail("Page not found");
context.Pages.Remove(page);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to delete page");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdatePageStatusAsync(long bookPageId, bool enabled, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken);
if (page is null)
return Result.Fail("Page not found");
page.UpdatedAt = DateTime.UtcNow;
page.Enabled = enabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to update page status");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> DeletePageAsync(long bookPageId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken);
if (page is null)
return Result.Fail("Page not found");
context.Pages.Remove(page);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to delete page");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdatePageAsync(long bookPageId, UpdateBookPage request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == bookPageId, cancellationToken);
if (page is null)
return Result.Fail("Page not found");
page.UpdatedAt = DateTime.UtcNow;
page.Type = request.Type;
page.ContentType = request.ContentType;
page.Number = request.Number;
page.Content = request.Content;
page.Notes = request.Notes;
page.References = request.References;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail("Failed to update page");
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreatePageAsync(long authorBookId, CreateBookPage request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken))
return Result.Fail<long>("Book not found");
if (await context.Pages.AnyAsync(p => p.AuthorBookId == authorBookId && p.Number == request.Number && p.Type == request.Type, cancellationToken))
return Result.Fail<long>("A page with the same number already exists for this book");
var page = context.Pages.Add(new Entities.BookPage
{
AuthorBookId = authorBookId,
Type = request.Type,
ContentType = request.ContentType,
Number = request.Number,
Content = request.Content,
Notes = request.Notes,
References = request.References,
Enabled = true
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(page.Entity.Id)
: Result.Fail<long>("Failed to create page");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<BookPage[]>> GetPagesAsync(long authorBookId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Books.AnyAsync(b => b.Id == authorBookId, cancellationToken))
return Result.Fail<BookPage[]>("Book not found");
var pages = await context.Pages.AsNoTracking()
.Where(p => p.AuthorBookId == authorBookId).ToArrayAsync(cancellationToken);
return pages?.Length > 0
? Result.Ok(pages.Select(p => p.ToModel()).ToArray())
: Result.Fail<BookPage[]>("No pages found for the specified book");
}
catch (Exception ex)
{
return Result.Fail<BookPage[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<BookPage>> GetPageByNumberAsync(long pageId, int number, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId && p.Number == number, cancellationToken);
return page is not null
? page.ToModel()
: Result.Fail<BookPage>("Page not found");
}
catch (Exception ex)
{
return Result.Fail<BookPage>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<BookPage>> GetPageAsync(long pageId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var page = await context.Pages.FirstOrDefaultAsync(p => p.Id == pageId, cancellationToken);
return page is not null
? page.ToModel()
: Result.Fail<BookPage>("Page not found");
}
catch (Exception ex)
{
return Result.Fail<BookPage>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -1,26 +0,0 @@
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
using LiteCharms.Features.MidrandBooks.Authors.Entities;
using LiteCharms.Features.MidrandBooks.Customers.Entities;
using LiteCharms.Features.MidrandBooks.Pages.Entities;
using LiteCharms.Features.MidrandBooks.Products.Entities;
namespace LiteCharms.Features.MidrandBooks.Postgres;
public class MidrandBooksDbContext(DbContextOptions<MidrandBooksDbContext> options) : DbContext(options)
{
public DbSet<Author> Authors => Set<Author>();
public DbSet<Product> Products => Set<Product>();
public DbSet<ProductPrice> Prices => Set<ProductPrice>();
public DbSet<AuthorBook> Books => Set<AuthorBook>();
public DbSet<BookPage> Pages => Set<BookPage>();
public DbSet<Contact> Contacts => Set<Contact>();
public DbSet<Address> Addresses => Set<Address>();
public DbSet<Customer> Customers => Set<Customer>();
}
@@ -1,21 +0,0 @@
using static LiteCharms.Features.MidrandBooks.Extensions.Postgres;
namespace LiteCharms.Features.MidrandBooks.Postgres;
public class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory<MidrandBooksDbContext>
{
public MidrandBooksDbContext CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets(typeof(MidrandBooksDbContext).Assembly)
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables()
.Build();
var optionsBuilder = new DbContextOptionsBuilder<MidrandBooksDbContext>();
optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName));
return new MidrandBooksDbContext(optionsBuilder.Options);
}
}
@@ -1,7 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
[EntityTypeConfiguration<ProductConfiguration, Product>]
public class Product : Models.Product
{
public virtual ICollection<ProductPrice> Prices { get; set; } = [];
}
@@ -1,22 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.ToTable("Products");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(f => f.Type).IsRequired();
builder.Property(f => f.Name).IsRequired().HasMaxLength(255);
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
builder.Property(f => f.Description).HasMaxLength(1024);
builder.Property(f => f.ImageUrl).HasMaxLength(1024);
builder.Property(f => f.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);
}
}
@@ -1,7 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
public class ProductPrice : Models.ProductPrice
{
public virtual Product Product { get; set; } = new();
}
@@ -1,22 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
public class ProductPriceConfiguration : IEntityTypeConfiguration<ProductPrice>
{
public void Configure(EntityTypeBuilder<ProductPrice> builder)
{
builder.ToTable("Prices");
builder.HasKey(f => f.Id);
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
builder.Property(f => f.UpdatedAt).HasDefaultValueSql("now()");
builder.Property(f => f.ProductId).IsRequired();
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.Discount).IsRequired().HasPrecision(18, 2);
builder.Property(f => f.Enabled).HasDefaultValue(false);
builder.HasOne(f => f.Product)
.WithMany(p => p.Prices)
.HasForeignKey(f => f.ProductId)
.OnDelete(DeleteBehavior.Restrict);
}
}
@@ -1,10 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public class CreateProductPrice
{
public long ProductId { get; set; }
public decimal Amount { get; set; }
public decimal Discount { get; set; }
}
@@ -1,32 +0,0 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public class Product
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public ProductTypes Type { get; set; }
public string? Name { get; set; }
public string? Summary { get; set; }
public string? Description { get; set; }
public string? ImageUrl { get; set; }
public string[]? ThumbnailUrls { get; set; }
public string[]? Categories { get; set; }
public ProductMetadata? Metadata { get; set; }
public ProductPrice? Price { get; set; }
public bool Enabled { get; set; }
}
@@ -1,18 +0,0 @@
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public class ProductPrice
{
public long Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public long ProductId { get; set; }
public decimal Amount { get; set; }
public decimal Discount { get; set; }
public bool Enabled { get; set; }
}
@@ -1,22 +0,0 @@
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Products.Models;
public record CreateProduct
{
public required ProductTypes Type { get; set; }
public required string Name { get; set; }
public required string Summary { get; set; }
public required string Description { get; set; }
public required string ImageUrl { get; set; }
public string[]? ThumbnailUrls { get; set; }
public string[]? Categories { get; set; }
public ProductMetadata? Metadata { get; set; }
}
@@ -1,256 +0,0 @@
using LiteCharms.Features.MidrandBooks.Abstractions;
using LiteCharms.Features.MidrandBooks.Extensions;
using LiteCharms.Features.MidrandBooks.Postgres;
using LiteCharms.Features.MidrandBooks.Products.Models;
using LiteCharms.Features.Models;
namespace LiteCharms.Features.MidrandBooks.Products;
public class ProductService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
{
public async ValueTask<Result> UpdateProductPriceStatusAsync(long productPriceId, bool isEnabled, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var productPrice = await context.Prices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
if (productPrice is null)
return Result.Fail(new Error($"Product price with ID {productPriceId} not found"));
productPrice.UpdatedAt = DateTime.UtcNow;
productPrice.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to change status of product price with ID {productPriceId}"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result> UpdateProductStatusAsync(long productId, bool isEnabled, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
if (product is null)
return Result.Fail(new Error($"Product with ID {productId} not found"));
product.UpdatedAt = DateTime.UtcNow;
product.Enabled = isEnabled;
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok()
: Result.Fail(new Error($"Failed to change status of product with ID {productId}"));
}
catch (Exception ex)
{
return Result.Fail(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Product[]>> SearchProductsAsync(ProductFilter filter, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var query = context.Products.AsQueryable();
if (!string.IsNullOrWhiteSpace(filter.Title))
query = query.Where(p => p.Name!.Contains(filter.Title));
if (!string.IsNullOrWhiteSpace(filter.Category))
query = query.Where(p => p.Categories!.Any(c => c == filter.Category));
if (!string.IsNullOrWhiteSpace(filter.Manufacturer))
query = query.Where(p => p.Metadata!.Manufacturer == filter.Manufacturer);
if (!string.IsNullOrWhiteSpace(filter.SerialNumber))
query = query.Where(p => p.Metadata!.SerialNumber == filter.SerialNumber);
if (filter.MinPrice > 0)
query = query.Where(p => p.Prices!.Any(pr => pr.Amount >= filter.MinPrice && pr.Amount <= filter.MaxPrice));
var products = await query.AsNoTracking().Where(p => p.Enabled).ToListAsync(cancellationToken);
return products?.Count > 0
? Result.Ok(products.Select(p => p.ToModel()).ToArray())
: Result.Fail<Product[]>("No products found.");
}
catch (Exception ex)
{
return Result.Fail<Product[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateProductPriceAsync(long productId, CreateProductPrice request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (!await context.Products.AnyAsync(p => p.Id == productId, cancellationToken))
return Result.Fail<long>($"Product with ID {productId} does not exist.");
var existingPrices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken);
if (existingPrices.Count > 0)
foreach (var existingPrice in existingPrices)
{
existingPrice.Enabled = false;
existingPrice.UpdatedAt = DateTime.UtcNow;
context.Prices.Update(existingPrice);
}
var price = context.Prices.Add(new Entities.ProductPrice
{
ProductId = productId,
Amount = request.Amount,
Discount = request.Discount,
Enabled = true,
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(price.Entity.Id)
: Result.Fail<long>("Failed to create product price.");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<long>> CreateProductAsync(CreateProduct request, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
if (await context.Products.AnyAsync(p => p.Name == request.Name, cancellationToken))
return Result.Fail<long>("A product with the same name already exists.");
if (request.Metadata is not null)
if (await context.Products.AnyAsync(p => p.Metadata!.SerialNumber == request.Metadata.SerialNumber, cancellationToken))
return Result.Fail<long>("A product with the same metadata already exists.");
var product = context.Products.Add(new Entities.Product
{
UpdatedAt = DateTime.UtcNow,
Type = request.Type,
Name = request.Name,
Summary = request.Summary,
Description = request.Description,
ImageUrl = request.ImageUrl,
ThumbnailUrls = request.ThumbnailUrls,
Metadata = request.Metadata,
Categories = request.Categories,
Enabled = true
});
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(product.Entity.Id)
: Result.Fail<long>("Failed to create product.");
}
catch (Exception ex)
{
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ProductPrice[]>> GetProductPriceAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Prices
.AsNoTracking()
.OrderByDescending(p => p.CreatedAt)
.ThenBy(p => p.UpdatedAt)
.FirstOrDefaultAsync(p => p.ProductId == productId, cancellationToken);
return product is not null
? Result.Ok(new[] { product.ToModel() })
: Result.Fail<ProductPrice[]>(new Error($"No price found for product with ID {productId}"));
}
catch (Exception ex)
{
return Result.Fail<ProductPrice[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<ProductPrice[]>> GetProductPricesAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var prices = await context.Prices.Where(p => p.ProductId == productId).ToListAsync(cancellationToken);
return prices?.Count > 0
? prices.Select(p => p.ToModel()).ToArray()
: Result.Fail<ProductPrice[]>(new Error($"No prices found for product with ID {productId}"));
}
catch (Exception ex)
{
return Result.Fail<ProductPrice[]>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Product>> GetProductAsync(long productId, CancellationToken cancellationToken = default)
{
try
{
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var product = await context.Products.AsNoTracking().FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
return product is null
? Result.Fail<Product>(new Error($"Product with ID {productId} not found."))
: Result.Ok(product.ToModel());
}
catch (Exception ex)
{
return Result.Fail<Product>(new Error(ex.Message).CausedBy(ex));
}
}
public async ValueTask<Result<Product[]>> GetProductsAsync(int offset, DateRange range, CancellationToken cancellationToken = default)
{
try
{
var fromDate = range.From.ToDateTime(TimeOnly.MinValue);
var toDate = range.To.ToDateTime(TimeOnly.MaxValue);
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
var products = await context.Products
.AsNoTracking()
.Include(p => p.Prices)
.OrderByDescending(p => p.CreatedAt)
.ThenByDescending(p => p.UpdatedAt)
.Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate)
.Skip(offset)
.Take(range.MaxRecords)
.AsSplitQuery()
.ToArrayAsync(cancellationToken);
return await context.SaveChangesAsync(cancellationToken) > 0
? Result.Ok(products.Select(p => p.ToModel()).ToArray())
: Result.Fail<Product[]>(new Error("Failed to retrieve products."));
}
catch (Exception ex)
{
return Result.Fail<Product[]>(new Error(ex.Message).CausedBy(ex));
}
}
}
@@ -1,22 +0,0 @@
{
"Email": {
"Credentials": {
"Username": "shop@litecharms.co.za"
},
"Port": 465,
"Host": "mail.litecharms.co.za",
"UseSsl": true
},
"Monitoring": {
"ApiKey": "",
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
"ServiceName": "MidrandBooks"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
@@ -1,38 +0,0 @@
using LiteCharms.Features.Extensions;
using LiteCharms.Features.TechShop.Extensions;
namespace LiteCharms.Features.TechShop.Tests;
public class Fixture : IDisposable
{
public IConfiguration Configuration { get; set; }
public IServiceProvider Services { get; set; }
public IMediator Mediator { get; set; }
public Fixture()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets<Fixture>()
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.Build();
Services = new ServiceCollection()
.AddMediator()
.AddLogging()
.AddTechShopServices()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddTechShopDatabase(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
}
public void Dispose() { }
}
@@ -1,51 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<UserSecretsId>fa06c1a6-2ba8-4c47-b19b-11e0f78e7b92</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<!-- Global Usings -->
<ItemGroup>
<Using Include="Mediator" />
<Using Include="Xunit.Abstractions" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiteCharms.Features.TechShop\LiteCharms.Features.TechShop.csproj" />
<ProjectReference Include="..\LiteCharms.Features\LiteCharms.Features.csproj" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -1,65 +0,0 @@
using LiteCharms.Features.Models;
using LiteCharms.Features.TechShop.Notifications;
using LiteCharms.Features.TechShop.Notifications.Events;
using static LiteCharms.Features.Extensions.Email;
namespace LiteCharms.Features.TechShop.Tests;
public class NotificationsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
private readonly NotificationService notificationService = fixture.Services.GetRequiredService<NotificationService>();
[Fact]
public async Task CreateNotificationCommand_ShouldSucceed()
{
Notifications.Models.CreateNotification request = new()
{
CorrelationId = Guid.CreateVersion7().ToString(),
CorrelationIdType = CorrelationIdTypes.None,
Direction = NotificationDirection.Outgoing,
Platform = NotificationPlatforms.Email,
Priority = Priorities.Medium,
Sender = "xUnit Test",
SenderAddress = "khwezi@mngoma.africa",
Recipient = $"{ShopEmailFromName} [Test]",
RecipientAddress = ShopEmailFromAddress,
Subject = "Test Message",
Message = "This is an automation test",
IsHtml = false,
IsInternal = true,
};
var createResult = await notificationService.CreateNotificationAsync(request);
Assert.True(createResult.IsSuccess);
foreach (var error in createResult.Errors) output.WriteLine(error.Message);
}
[Fact]
public async Task GetNotifications_ShouldReturn_AllNotifications()
{
DateRange range = new()
{
From = DateOnly.FromDateTime(new DateTime(2026, 04, 01, 0, 0, 0, DateTimeKind.Utc)),
To = DateOnly.FromDateTime(DateTime.UtcNow),
MaxRecords = 10
};
var getResult = await notificationService.GetNotificationsAsync(range);
Assert.True(getResult.IsSuccess);
foreach (var error in getResult.Errors) output.WriteLine(error.Message);
}
[Fact]
public async Task ProcessEmailNotificationsEvent_ShouldSucceed()
{
var notification = ProcessEmailNotificationsEvent.Create();
await fixture.Mediator.Publish(notification);
Assert.True(true);
}
}
@@ -1,19 +0,0 @@
using LiteCharms.Features.TechShop.Products;
namespace LiteCharms.Features.TechShop.Tests;
public class ProductsFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
[Fact]
public async Task GetProductsAsync_ReturnsProducts()
{
var productService = fixture.Services.GetRequiredService<ProductService>();
var result = await productService.GetProductsAsync();
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
output.WriteLine($"Retrieved {result.Value.Length} products.");
}
}
@@ -1,34 +0,0 @@
{
"BookshopS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop",
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
},
"BookshopQuotesS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop.quotes",
"CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za"
},
"Email": {
"Credentials": {
"Username": "shop@litecharms.co.za"
},
"Port": 465,
"Host": "mail.litecharms.co.za",
"UseSsl": true
},
"Monitoring": {
"ApiKey": "",
"Address": "http://aspire-dashboard-service.aspire.svc.cluster.local:18889",
"ServiceName": "LiteCharms.LeadGenerator"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
@@ -1,22 +0,0 @@
using LiteCharms.Features.TechShop.HealthChecks;
using static LiteCharms.Features.TechShop.Extensions.Postgres;
namespace LiteCharms.Features.TechShop.Extensions;
public static class HealthChecks
{
public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<ShopQuartzHealthCheck>("ShopQuartz");
return services;
}
public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresShopHealthCheck>(TechShopDbConfigName);
return services;
}
}
@@ -1,16 +0,0 @@
using LiteCharms.Features.TechShop.Postgres;
namespace LiteCharms.Features.TechShop.Extensions;
public static class Postgres
{
public const string TechShopDbConfigName = "PostgresShop";
public static IServiceCollection AddTechShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<ShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString(TechShopDbConfigName)));
return services;
}
}
@@ -1,27 +0,0 @@
using LiteCharms.Features.TechShop.CartPackages;
using LiteCharms.Features.TechShop.Customers;
using LiteCharms.Features.TechShop.Leads;
using LiteCharms.Features.TechShop.Notifications;
using LiteCharms.Features.TechShop.Orders;
using LiteCharms.Features.TechShop.Products;
using LiteCharms.Features.TechShop.Quotes;
using LiteCharms.Features.TechShop.ShoppingCarts;
namespace LiteCharms.Features.TechShop.Extensions;
public static class Shop
{
public static IServiceCollection AddTechShopServices(this IServiceCollection services)
{
services.AddSingleton<PackageService>()
.AddSingleton<LeadService>()
.AddSingleton<NotificationService>()
.AddSingleton<OrderService>()
.AddSingleton<ProductService>()
.AddSingleton<QuoteService>()
.AddSingleton<ShoppingCartService>()
.AddSingleton<CustomerService>();
return services;
}
}
@@ -1,28 +0,0 @@
using static LiteCharms.Features.Extensions.Quartz;
namespace LiteCharms.Features.TechShop.HealthChecks;
public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var scheduler = await schedulerFactory.GetScheduler(TechShopSchedulerName, cancellationToken);
if(scheduler == null)
return HealthCheckResult.Unhealthy($"Scheduler with name '{TechShopSchedulerName}' not found.");
if (!scheduler.IsStarted)
return HealthCheckResult.Unhealthy($"{TechShopSchedulerName} Quartz scheduler is not running");
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
return HealthCheckResult.Healthy($"{TechShopSchedulerName} Quartz scheduler is ready");
}
catch (SchedulerException)
{
return HealthCheckResult.Unhealthy($"{TechShopSchedulerName} Quartz scheduler cannot connect to the store");
}
}
}
@@ -1,171 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\LiteCharms.snk</AssemblyOriginatorKeyFile>
<UserSecretsId>fbd8f4a2-0420-44e2-baff-4678d9e7eee1</UserSecretsId>
</PropertyGroup>
<!-- Nuget Package Details -->
<PropertyGroup>
<PackageId>LiteCharms.Features.TechShop</PackageId>
<Version>1.0.20</Version>
<Authors>Khwezi Mngoma</Authors>
<Company>Lite Charms (PTY) Ltd</Company>
<Description>TechShop feature components for Lite Charms applications.</Description>
<PackageProjectUrl>https://gitea.khongisa.co.za/litecharms/components</PackageProjectUrl>
<RepositoryUrl>https://gitea.khongisa.co.za/litecharms/components.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageTags>utility;dotnet</PackageTags>
<PackageIcon>icon.png</PackageIcon>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Addresses\**" />
<EmbeddedResource Remove="Addresses\**" />
<None Remove="Addresses\**" />
</ItemGroup>
<ItemGroup>
<None Include="..\LICENSE" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<!-- Quartz Scheduler-->
<ItemGroup>
<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="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>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -1,776 +0,0 @@
// <auto-generated />
using System;
using LiteCharms.Features.TechShop.Postgres;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
[DbContext(typeof(ShopDbContext))]
[Migration("20260515055221_FixedLeadCustomerRelationship")]
partial class FixedLeadCustomerRelationship
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Package", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("ProductPriceId");
b.ToTable("PackageItem", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("WebClickId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Leads", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Notification", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Orders", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("OrderRefunds", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.HasKey("Id");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId");
b.ToTable("ProductPrices", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.HasIndex("ShoppingCartId")
.IsUnique();
b.ToTable("Quotes", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.ToTable("ShoppingCarts", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductPriceId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartItems", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartPackages", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
{
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
.WithMany("PackageItems")
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Package");
b.Navigation("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Leads")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Orders")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
{
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithMany("Refunds")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product")
.WithMany("ProductPrices")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Quotes")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithOne("Quote")
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId");
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithOne("Quote")
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId");
b.Navigation("Customer");
b.Navigation("Order");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("ShoppingCarts")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithOne("ShoppingCart")
.HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Customer");
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartItems")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProductPrice");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
{
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartPackages")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Package");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
{
b.Navigation("PackageItems");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
{
b.Navigation("Leads");
b.Navigation("Orders");
b.Navigation("Quotes");
b.Navigation("ShoppingCarts");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.Navigation("Quote");
b.Navigation("Refunds");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
{
b.Navigation("ProductPrices");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.Navigation("Quote");
b.Navigation("ShoppingCartItems");
b.Navigation("ShoppingCartPackages");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,166 +0,0 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
/// <inheritdoc />
public partial class FixedLeadCustomerRelationship : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Leads_Customer_CustomerId",
table: "Leads");
migrationBuilder.DropForeignKey(
name: "FK_Leads_Customers_CustomerId1",
table: "Leads");
migrationBuilder.DropForeignKey(
name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1",
table: "ShoppingCartItems");
migrationBuilder.DropForeignKey(
name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1",
table: "ShoppingCartItems");
migrationBuilder.DropTable(
name: "Customer");
migrationBuilder.DropIndex(
name: "IX_ShoppingCartItems_ProductPriceId1",
table: "ShoppingCartItems");
migrationBuilder.DropIndex(
name: "IX_ShoppingCartItems_ShoppingCartId1",
table: "ShoppingCartItems");
migrationBuilder.DropIndex(
name: "IX_Leads_CustomerId1",
table: "Leads");
migrationBuilder.DropColumn(
name: "ProductPriceId1",
table: "ShoppingCartItems");
migrationBuilder.DropColumn(
name: "ShoppingCartId1",
table: "ShoppingCartItems");
migrationBuilder.DropColumn(
name: "CustomerId1",
table: "Leads");
migrationBuilder.AddForeignKey(
name: "FK_Leads_Customers_CustomerId",
table: "Leads",
column: "CustomerId",
principalTable: "Customers",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Leads_Customers_CustomerId",
table: "Leads");
migrationBuilder.AddColumn<Guid>(
name: "ProductPriceId1",
table: "ShoppingCartItems",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "ShoppingCartId1",
table: "ShoppingCartItems",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "CustomerId1",
table: "Leads",
type: "uuid",
nullable: true);
migrationBuilder.CreateTable(
name: "Customer",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
Active = table.Column<bool>(type: "boolean", nullable: false),
Address = table.Column<string>(type: "text", nullable: true),
City = table.Column<string>(type: "text", nullable: true),
Company = table.Column<string>(type: "text", nullable: true),
Country = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Discord = table.Column<string>(type: "text", nullable: true),
Email = table.Column<string>(type: "text", nullable: true),
LastName = table.Column<string>(type: "text", nullable: true),
LinkedIn = table.Column<string>(type: "text", nullable: true),
Name = table.Column<string>(type: "text", nullable: true),
Phone = table.Column<string>(type: "text", nullable: true),
PostalCode = table.Column<string>(type: "text", nullable: true),
Region = table.Column<string>(type: "text", nullable: true),
Slack = table.Column<string>(type: "text", nullable: true),
Tax = table.Column<string>(type: "text", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Website = table.Column<string>(type: "text", nullable: true),
Whatsapp = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Customer", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_ShoppingCartItems_ProductPriceId1",
table: "ShoppingCartItems",
column: "ProductPriceId1");
migrationBuilder.CreateIndex(
name: "IX_ShoppingCartItems_ShoppingCartId1",
table: "ShoppingCartItems",
column: "ShoppingCartId1");
migrationBuilder.CreateIndex(
name: "IX_Leads_CustomerId1",
table: "Leads",
column: "CustomerId1");
migrationBuilder.AddForeignKey(
name: "FK_Leads_Customer_CustomerId",
table: "Leads",
column: "CustomerId",
principalTable: "Customer",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Leads_Customers_CustomerId1",
table: "Leads",
column: "CustomerId1",
principalTable: "Customers",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_ShoppingCartItems_ProductPrices_ProductPriceId1",
table: "ShoppingCartItems",
column: "ProductPriceId1",
principalTable: "ProductPrices",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_ShoppingCartItems_ShoppingCarts_ShoppingCartId1",
table: "ShoppingCartItems",
column: "ShoppingCartId1",
principalTable: "ShoppingCarts",
principalColumn: "Id");
}
}
}
@@ -1,788 +0,0 @@
// <auto-generated />
using System;
using LiteCharms.Features.TechShop.Postgres;
using LiteCharms.Features.TechShop.Products.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
[DbContext(typeof(ShopDbContext))]
[Migration("20260520191059_AddedProductMetadata")]
partial class AddedProductMetadata
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Package", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTimeOffset>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("ProductPriceId");
b.ToTable("PackageItem", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Address")
.HasColumnType("text");
b.Property<string>("City")
.HasColumnType("text");
b.Property<string>("Company")
.HasColumnType("text");
b.Property<string>("Country")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Discord")
.HasColumnType("text");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LastName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("LinkedIn")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Phone")
.HasColumnType("text");
b.Property<string>("PostalCode")
.HasColumnType("text");
b.Property<string>("Region")
.HasColumnType("text");
b.Property<string>("Slack")
.HasColumnType("text");
b.Property<string>("Tax")
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Website")
.HasColumnType("text");
b.Property<string>("Whatsapp")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Customers", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<long?>("AdGroupId")
.HasColumnType("bigint");
b.Property<long?>("AdName")
.HasColumnType("bigint");
b.Property<string>("AppClickId")
.HasColumnType("text");
b.Property<string>("AttributionHash")
.IsRequired()
.HasColumnType("text");
b.Property<long?>("CampaignId")
.HasColumnType("bigint");
b.Property<string>("ClickId")
.HasColumnType("text");
b.Property<string>("ClickLocation")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid?>("CustomerId")
.HasColumnType("uuid");
b.Property<long?>("FeedItemId")
.HasColumnType("bigint");
b.Property<string>("Source")
.HasColumnType("text");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<long?>("TargetId")
.HasColumnType("bigint");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("WebClickId")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Leads", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<string>("CorrelationId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("CorrelationIdType")
.HasColumnType("integer");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<int>("Direction")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Errors")
.HasColumnType("jsonb");
b.Property<bool>("HasError")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsHtml")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<bool>("IsInternal")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(true);
b.Property<string>("Message")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Platform")
.HasColumnType("integer");
b.Property<int>("Priority")
.HasColumnType("integer");
b.Property<bool>("Processed")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<string>("RecipientAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("RecipientName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderAddress")
.IsRequired()
.HasColumnType("text");
b.Property<string>("SenderName")
.HasColumnType("text");
b.Property<string>("Subject")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Notification", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.PrimitiveCollection<string>("Notes")
.HasColumnType("jsonb");
b.PrimitiveCollection<string>("Requirements")
.HasColumnType("jsonb");
b.Property<int>("Status")
.HasColumnType("integer");
b.PrimitiveCollection<string>("Terms")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.ToTable("Orders", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<decimal>("Amount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("OrderRefunds", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.ValueGeneratedOnAdd()
.HasColumnType("boolean")
.HasDefaultValue(false);
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<string>("ImageUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<ProductMetadata>("Metadata")
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("character varying(512)");
b.PrimitiveCollection<string>("Thumbnails")
.HasColumnType("jsonb");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.ToTable("Products", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<bool>("Active")
.HasColumnType("boolean");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<decimal>("Discount")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<decimal>("Price")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)");
b.Property<Guid>("ProductId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductId");
b.ToTable("ProductPrices", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<DateTime?>("ExpiredAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("InvoiceUrl")
.HasMaxLength(2048)
.HasColumnType("character varying(2048)");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<string>("Reason")
.HasColumnType("text");
b.Property<Guid?>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<int>("Status")
.HasColumnType("integer");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.HasIndex("ShoppingCartId")
.IsUnique();
b.ToTable("Quotes", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("CustomerId")
.HasColumnType("uuid");
b.Property<Guid?>("OrderId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("CustomerId");
b.HasIndex("OrderId")
.IsUnique();
b.ToTable("ShoppingCarts", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("ProductPriceId")
.HasColumnType("uuid");
b.Property<int>("Quantity")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasDefaultValue(1);
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("timestamp with time zone");
b.HasKey("Id");
b.HasIndex("ProductPriceId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartItems", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("timestamp with time zone")
.HasDefaultValueSql("now()");
b.Property<Guid>("PackageId")
.HasColumnType("uuid");
b.Property<Guid>("ShoppingCartId")
.HasColumnType("uuid");
b.HasKey("Id");
b.HasIndex("PackageId");
b.HasIndex("ShoppingCartId");
b.ToTable("ShoppingCartPackages", (string)null);
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
{
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
.WithMany("PackageItems")
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Package");
b.Navigation("ProductPrice");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Leads")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict);
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Orders")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
{
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithMany("Refunds")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
{
b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product")
.WithMany("ProductPrices")
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Product");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("Quotes")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithOne("Quote")
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId");
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithOne("Quote")
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId");
b.Navigation("Customer");
b.Navigation("Order");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
.WithMany("ShoppingCarts")
.HasForeignKey("CustomerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
.WithOne("ShoppingCart")
.HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("Customer");
b.Navigation("Order");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
{
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
.WithMany()
.HasForeignKey("ProductPriceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartItems")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProductPrice");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
{
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
.WithMany()
.HasForeignKey("PackageId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
.WithMany("ShoppingCartPackages")
.HasForeignKey("ShoppingCartId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Package");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
{
b.Navigation("PackageItems");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
{
b.Navigation("Leads");
b.Navigation("Orders");
b.Navigation("Quotes");
b.Navigation("ShoppingCarts");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
{
b.Navigation("Quote");
b.Navigation("Refunds");
b.Navigation("ShoppingCart");
});
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
{
b.Navigation("ProductPrices");
});
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
{
b.Navigation("Quote");
b.Navigation("ShoppingCartItems");
b.Navigation("ShoppingCartPackages");
});
#pragma warning restore 612, 618
}
}
}
@@ -1,71 +0,0 @@
using System;
using LiteCharms.Features.TechShop.Products.Models;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LiteCharms.Features.TechShop.Postgres.Migrations
{
/// <inheritdoc />
public partial class AddedProductMetadata : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<bool>(
name: "Active",
table: "Products",
type: "boolean",
nullable: false,
defaultValue: false,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: true);
migrationBuilder.AddColumn<DateTime>(
name: "CreatedAt",
table: "Products",
type: "timestamp with time zone",
nullable: false,
defaultValueSql: "now()");
migrationBuilder.AddColumn<ProductMetadata>(
name: "Metadata",
table: "Products",
type: "jsonb",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "UpdatedAt",
table: "Products",
type: "timestamp with time zone",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CreatedAt",
table: "Products");
migrationBuilder.DropColumn(
name: "Metadata",
table: "Products");
migrationBuilder.DropColumn(
name: "UpdatedAt",
table: "Products");
migrationBuilder.AlterColumn<bool>(
name: "Active",
table: "Products",
type: "boolean",
nullable: false,
defaultValue: true,
oldClrType: typeof(bool),
oldType: "boolean",
oldDefaultValue: false);
}
}
}
@@ -1,39 +0,0 @@
using LiteCharms.Features.TechShop.CartPackages.Entities;
using LiteCharms.Features.TechShop.Customers.Entities;
using LiteCharms.Features.TechShop.Leads.Entities;
using LiteCharms.Features.TechShop.Notifications.Entities;
using LiteCharms.Features.TechShop.Orders.Entities;
using LiteCharms.Features.TechShop.Products.Entities;
using LiteCharms.Features.TechShop.Quotes.Entities;
using LiteCharms.Features.TechShop.ShoppingCarts.Entities;
namespace LiteCharms.Features.TechShop.Postgres;
public class ShopDbContext(DbContextOptions<ShopDbContext> options) : DbContext(options)
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Lead> Leads { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderRefund> OrderRefunds { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<ProductPrice> ProductPrices { get; set; }
public DbSet<Notification> Notifications { get; set; }
public DbSet<Quote> Quotes { get; set; }
public DbSet<ShoppingCart> ShoppingCarts { get; set; }
public DbSet<ShoppingCartItem> ShoppingCartItems { get; set; }
public DbSet<Package> Packages { get; set; }
public DbSet<PackageItem> PackageItems { get; set; }
public DbSet<ShoppingCartPackage> ShoppingCartPackages { get; set; }
}
@@ -1,42 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace LiteCharms.Features.TechShop.Products.Models;
public class CreateProductModel
{
[MaxLength(128)]
[Required(ErrorMessage = "Product name is required.")]
public string? Name { get; set; }
[MaxLength(512)]
[Required(ErrorMessage = "Summary is required.")]
public string? Summary { get; set; }
[MaxLength(2048)]
[Required(ErrorMessage = "Description is required.")]
public string? Description { get; set; }
[Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than zero.")]
public decimal Price { get; set; }
[MaxLength(128)]
[Required(ErrorMessage = "Author metadata is required.")]
public string? Author { get; set; }
[Required(ErrorMessage = "Publication Date is required.")]
public DateTime PublishDate { get; set; } = DateTime.Today;
[MaxLength(255)]
[Required(ErrorMessage = "Copyright Information field is required.")]
public string? CopyrightInfo { get; set; }
[MaxLength(128)]
[Required(ErrorMessage = "ISBN code is required.")]
[RegularExpression(@"^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$", ErrorMessage = "Please enter a valid ISBN-10 or ISBN-13 string.")]
public string? Isbn { get; set; }
[Required(ErrorMessage = "Primary image is required.")]
public string? ImageUrl { get; set; }
public List<string> Thumbnails { get; set; } = [];
}
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Tests;
public class Fixture : IDisposable
public class CommonFixture : IDisposable
{
public IConfiguration Configuration { get; set; }
@@ -10,22 +10,22 @@ public class Fixture : IDisposable
public IMediator Mediator { get; set; }
public Fixture()
public CommonFixture()
{
Configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets<Fixture>()
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
.AddJsonFile("appsettings.json")
.AddUserSecrets<CommonFixture>()
.AddEnvironmentVariables()
.Build();
Services = new ServiceCollection()
Services = new ServiceCollection()
.AddMediator()
.AddLogging()
.AddLogging()
.AddShopServices()
.AddEmailServiceBus()
.AddGarageS3(Configuration)
.AddShopDatabase(Configuration)
.AddEmailServices(Configuration)
.AddSingleton(Configuration)
.BuildServiceProvider();
Mediator = Services.GetRequiredService<IMediator>();
@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.1">
<PackageReference Include="coverlet.collector" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -27,10 +27,10 @@
<!-- Global Usings -->
<ItemGroup>
<Using Include="Mediator" />
<Using Include="Xunit.Abstractions" />
<Using Include="Microsoft.Extensions.DependencyInjection" />
<Using Include="Microsoft.Extensions.Configuration" />
<Using Include="Mediator"/>
<Using Include="Xunit.Abstractions"/>
<Using Include="Microsoft.Extensions.DependencyInjection"/>
<Using Include="Microsoft.Extensions.Configuration"/>
</ItemGroup>
<ItemGroup>
@@ -43,7 +43,7 @@
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
@@ -0,0 +1,35 @@
using LiteCharms.Features.Shop.Notifications;
namespace LiteCharms.Features.Tests;
public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture<CommonFixture>
{
private readonly NotificationService notificationService = fixture.Services.GetRequiredService<NotificationService>();
[Fact]
public async Task CreateNotificationCommand_ShouldSucceed()
{
Shop.Notifications.Models.CreateNotification request = new()
{
CorrelationId = Guid.CreateVersion7().ToString(),
CorrelationIdType = Shop.CorrelationIdTypes.None,
Direction = Shop.NotificationDirection.Outgoing,
Platform = Shop.NotificationPlatforms.Email,
Priority = Shop.Priorities.Medium,
Sender = "xUnit Test",
SenderAddress = "khwezi@mngoma.africa",
Recipient = $"{Email.Extensions.Constants.ShopEmailFromName} [Test]",
RecipientAddress = Email.Extensions.Constants.ShopEmailFromAddress,
Subject = "Test Message",
Message = "This is an automation test",
IsHtml = false,
IsInternal = true,
};
var createResult = await notificationService.CreateNotificationAsync(request);
Assert.True(createResult.IsSuccess);
foreach (var error in createResult.Errors) output.WriteLine(error.Message);
}
}
@@ -1,54 +0,0 @@
using LiteCharms.Features.S3.Abstractions;
namespace LiteCharms.Features.Tests;
public class S3ServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
{
[Fact]
public async Task BookshopS3Service_MustReturnUrl()
{
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopBucketName);
var fileName = "appsettings.json";
string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
Assert.True(File.Exists(path));
var stream = File.OpenRead(path);
var result = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName));
Assert.True(result.IsSuccess);
Assert.NotNull(result.Value);
Assert.NotEmpty(result.Value);
output.WriteLine(result.Value);
}
[Fact]
public async Task BookshopS3Service_MustDeleteFile()
{
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopBucketName);
var fileName = "appsettings.json";
string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
Assert.True(File.Exists(path));
var stream = File.OpenRead(path);
var uploadResult = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName));
Assert.True(uploadResult.IsSuccess);
Assert.NotNull(uploadResult.Value);
Assert.NotEmpty(uploadResult.Value);
var fileKey = uploadResult.Value.Split('/').Last();
var deleteResult = await service!.DeleteFileAsync(fileKey);
Assert.True(deleteResult.IsSuccess);
}
}
@@ -1,16 +1,4 @@
{
"BookshopS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop",
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
},
"BookshopQuotesS3Settings": {
"ServiceUrl": "http://192.168.1.177:30900",
"Region": "garage",
"BucketName": "bookshop.quotes",
"CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za"
},
"Email": {
"Credentials": {
"Username": "shop@litecharms.co.za"
+3 -3
View File
@@ -1,6 +1,7 @@
using LiteCharms.Features.Email.Configuration;
using LiteCharms.Features.Email.Extensions;
using LiteCharms.Features.Email.Models;
using LiteCharms.Features.Shop;
namespace LiteCharms.Features.Email;
@@ -34,9 +35,8 @@ public class EmailService(IOptions<SmtpSettings> options) : IDisposable
var bodyBuilder = new BodyBuilder();
if (message.Body!.Properties.HasAttachments)
foreach (var attachment in message.Body?.Attachments!)
bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken);
foreach (var attachment in message.Body?.Attachments!)
bodyBuilder.Attachments.Add(attachment.Name!, attachment.FileStream!, cancellationToken);
if (!message.Body.Properties.IsHtml) bodyBuilder.TextBody = message.Body.Message;
if (message.Body.Properties.IsHtml) bodyBuilder.HtmlBody = message.Body.Message;
@@ -1,12 +1,14 @@
using static LiteCharms.Features.Extensions.Email;
using LiteCharms.Features.Shop;
using LiteCharms.Features.Shop.Notifications;
using static LiteCharms.Features.Email.Extensions.Constants;
namespace LiteCharms.Features.TechShop.Notifications.Events.Handlers;
namespace LiteCharms.Features.Email.Events.Handlers;
public class SendShopEmailEnquiryEventHandler(NotificationService notificationService) :
INotificationHandler<SendShopEmailEnquiryEvent>
{
public async ValueTask Handle(SendShopEmailEnquiryEvent notification, CancellationToken cancellationToken) =>
await notificationService.CreateNotificationAsync(new Models.CreateNotification
await notificationService.CreateNotificationAsync(new Shop.Notifications.Models.CreateNotification
{
CorrelationId = notification.CorrelationId,
CorrelationIdType = CorrelationIdTypes.None,
@@ -1,6 +1,7 @@
using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Shop;
namespace LiteCharms.Features.TechShop.Notifications.Events;
namespace LiteCharms.Features.Email.Events;
public class SendShopEmailEnquiryEvent : EventBase, IEvent
{
@@ -0,0 +1,8 @@
namespace LiteCharms.Features.Email.Extensions;
public static class Constants
{
public const string ShopSchedulerName = "shop";
public const string ShopEmailFromName = "Khongisa Shop";
public const string ShopEmailFromAddress = "shop@litecharms.co.za";
}
@@ -2,7 +2,7 @@
namespace LiteCharms.Features.Email.Models;
public sealed class EmailEnquiryModel
public sealed class EmailEnquiry
{
[Required]
[MinLength(2)]
+3 -1
View File
@@ -1,4 +1,6 @@
namespace LiteCharms.Features.Email.Models;
using LiteCharms.Features.Shop;
namespace LiteCharms.Features.Email.Models;
public class Response
{
-116
View File
@@ -1,116 +0,0 @@
namespace LiteCharms.Features;
public enum ShippingProviderTypes : int
{
Dsv = 0,
Pargo = 1,
Ram = 2,
TheCourierGuy = 3,
Paxi = 4,
FastWay = 5,
MdsCollivery = 6,
PostNet = 7,
Aramex = 8,
DHL = 9,
FedEx = 10,
UPS = 11,
USPS = 12,
AmazonLogistics = 13,
LocalCourier = 14,
Other = 15
}
public enum ShippingStatuses : int
{
Pending = 0,
Shipped = 1,
Delivered = 2,
Returned = 3,
Cancelled = 4,
}
public enum RefundTypes : int
{
Full = 0,
Partial = 1,
StoreCredit = 2,
Exchange = 3,
Other = 4
}
public enum RefundStatus : int
{
Pending = 0,
Approved = 1,
Rejected = 2,
Completed = 3,
Failed = 4,
}
public enum OrderStatus : int
{
Pending = 0,
Completed = 1,
Cancelled = 2,
Failed = 3,
Refunded = 4,
Error = 5,
OnHold = 6,
}
public enum ContactTypes : int
{
Personal = 0,
Business = 1,
Other = 2
}
public enum AddressType
{
Billing = 1,
Shipping = 2,
Other = 3
}
public enum AddressBuildingTypes : int
{
Residential = 0,
Commercial = 1,
Industrial = 2,
MixedUse = 3,
Agricultural = 4,
Institutional = 5,
Recreational = 6,
}
public enum SocialMediaTypes : int
{
Twitter = 0,
Facebook = 1,
Instagram = 2,
LinkedIn = 3,
TikTok = 4,
YouTube = 5,
Pinterest = 6,
Reddit = 7,
Tumblr = 8
}
public enum EmailStatuses : int
{
GeneralError = 0,
AuthenticationError = 1,
ProtocolError = 2,
Connected = 3,
Disconnected = 4,
TooManyConnections = 5,
ConnectionAborted = 6,
Success = 7
}
public enum Priorities : int
{
Low = 0,
Medium = 1,
High = 2,
}
+1 -4
View File
@@ -4,10 +4,7 @@ using LiteCharms.Features.Email.Configuration;
namespace LiteCharms.Features.Extensions;
public static class Email
{
public const string ShopEmailFromName = "Khongisa Shop";
public const string ShopEmailFromAddress = "shop@litecharms.co.za";
{
public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration)
{
services.Configure<SmtpSettings>(configuration.GetSection("Email"));
@@ -1,18 +1,17 @@
using LiteCharms.Features.TechShop.CartPackages.Models;
using LiteCharms.Features.TechShop.Customers.Models;
using LiteCharms.Features.TechShop.Leads.Models;
using LiteCharms.Features.TechShop.Notifications.Models;
using LiteCharms.Features.TechShop.Orders.Models;
using LiteCharms.Features.TechShop.Products.Entities;
using LiteCharms.Features.TechShop.Products.Models;
using LiteCharms.Features.TechShop.Quotes.Models;
using LiteCharms.Features.TechShop.ShoppingCarts.Models;
using LiteCharms.Features.Shop.CartPackages.Models;
using LiteCharms.Features.Shop.Customers.Models;
using LiteCharms.Features.Shop.Leads.Models;
using LiteCharms.Features.Shop.Notifications.Models;
using LiteCharms.Features.Shop.Orders.Models;
using LiteCharms.Features.Shop.Products.Models;
using LiteCharms.Features.Shop.Quotes.Models;
using LiteCharms.Features.Shop.ShoppingCarts.Models;
namespace LiteCharms.Features.TechShop.Extensions;
namespace LiteCharms.Features.Extensions;
public static class Mappers
public static class EntityModeMappers
{
public static ShoppingCartPackage ToModel(this ShoppingCarts.Entities.ShoppingCartPackage entity) =>
public static ShoppingCartPackage ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage entity) =>
new()
{
Id = entity.Id,
@@ -21,7 +20,7 @@ public static class Mappers
ShoppingCartId = entity.ShoppingCartId
};
public static PackageItem ToModel(this CartPackages.Entities.PackageItem entity) =>
public static PackageItem ToModel(this Features.Shop.CartPackages.Entities.PackageItem entity) =>
new()
{
Id = entity.Id,
@@ -31,7 +30,7 @@ public static class Mappers
ProductPriceId = entity.ProductPriceId
};
public static Package ToModel(this CartPackages.Entities.Package entity) =>
public static Package ToModel(this Features.Shop.CartPackages.Entities.Package entity) =>
new()
{
Id = entity.Id,
@@ -44,7 +43,7 @@ public static class Mappers
Summary = entity.Summary
};
public static ShoppingCartItem ToModel(this ShoppingCarts.Entities.ShoppingCartItem entity) =>
public static ShoppingCartItem ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCartItem entity) =>
new()
{
Id = entity.Id,
@@ -55,7 +54,7 @@ public static class Mappers
ShoppingCartId = entity.ShoppingCartId
};
public static ShoppingCart ToModel(this ShoppingCarts.Entities.ShoppingCart entity) =>
public static ShoppingCart ToModel(this Features.Shop.ShoppingCarts.Entities.ShoppingCart entity) =>
new()
{
Id = entity.Id,
@@ -65,7 +64,7 @@ public static class Mappers
OrderId = entity.OrderId
};
public static Quote ToModel(this Quotes.Entities.Quote entity) =>
public static Quote ToModel(this Features.Shop.Quotes.Entities.Quote entity) =>
new()
{
Id = entity.Id,
@@ -80,7 +79,7 @@ public static class Mappers
OrderId = entity.OrderId
};
public static Notification ToModel(this Notifications.Entities.Notification entity) =>
public static Notification ToModel(this Features.Shop.Notifications.Entities.Notification entity) =>
new()
{
Id = entity.Id,
@@ -104,7 +103,7 @@ public static class Mappers
Errors = entity.Errors
};
public static Customer ToModel(this Customers.Entities.Customer entity) =>
public static Customer ToModel(this Features.Shop.Customers.Entities.Customer entity) =>
new()
{
Id = entity.Id,
@@ -129,7 +128,7 @@ public static class Mappers
Whatsapp = entity.Whatsapp
};
public static Lead ToModel(this Leads.Entities.Lead entity) =>
public static Lead ToModel(this Features.Shop.Leads.Entities.Lead entity) =>
new()
{
Id = entity.Id,
@@ -150,7 +149,7 @@ public static class Mappers
Status = entity.Status
};
public static Order ToModel(this Orders.Entities.Order entity) =>
public static Order ToModel(this Features.Shop.Orders.Entities.Order entity) =>
new()
{
Id = entity.Id,
@@ -164,7 +163,7 @@ public static class Mappers
InvoiceUrl = entity.InvoiceUrl
};
public static OrderRefund ToModel(this Orders.Entities.OrderRefund entity) =>
public static OrderRefund ToModel(this Features.Shop.Orders.Entities.OrderRefund entity) =>
new()
{
Id = entity.Id,
@@ -174,21 +173,19 @@ public static class Mappers
Amount = entity.Amount
};
public static Products.Models.Product ToModel(this Products.Entities.Product entity) =>
public static Product ToModel(this Features.Shop.Products.Entities.Product entity) =>
new()
{
Id = entity.Id,
CreatedAt = entity.CreatedAt,
Id = entity.Id,
Name = entity.Name,
Description = entity.Description,
Active = entity.Active,
Summary = entity.Summary,
ImageUrl = entity.ImageUrl,
Thumbnails = entity.Thumbnails,
Metadata = entity.Metadata,
Thumbnails = entity.Thumbnails
};
public static Products.Models.ProductPrice ToModel(this Products.Entities.ProductPrice entity) =>
public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) =>
new()
{
Id = entity.Id,
+1 -7
View File
@@ -2,12 +2,6 @@
public static class Hash
{
public static Func<string?, string?> StringToSha256Hash = (input) =>
public static Func<string?, string?> GenerateSha256HashString = (input) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
public static Func<Stream, string?> StreamToSha256Hash = (stream) =>
Convert.ToHexString(SHA256.HashData(stream));
public static Func<byte[], string?> BytesToSha256Hash = (bytes) =>
Convert.ToHexString(SHA256.HashData(bytes));
}
@@ -0,0 +1,34 @@
using LiteCharms.Features.HealthChecks;
namespace LiteCharms.Features.Extensions;
public static class HealthChecks
{
public static IServiceCollection AddQuartzHealtchCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<QuartzHealthCheck>("Quartz");
return services;
}
public static IServiceCollection AddPostgresHealtchCheck(this IServiceCollection services)
{
services.AddHealthChecks().AddCheck<PostgresHealthCheck>("PostgreSQL");
return services;
}
public static IServiceCollection AddHealthChecksSupport(this IServiceCollection services, IConfiguration configuration)
{
services.AddHealthChecks()
.AddCheck("Self", () => HealthCheckResult.Healthy());
//services.AddHealthChecksUI(setup =>
//{
// setup.AddHealthCheckEndpoint("Lead Generator", $"{configuration["ASPNETCORE_URLS"]}/health");
// setup.SetEvaluationTimeInSeconds(15);
//}).AddInMemoryStorage(databaseName: "healthuidb");
return services;
}
}
+10 -2
View File
@@ -1,6 +1,14 @@
namespace LiteCharms.Features.Extensions;
using LiteCharms.Features.Shop.Postgres;
namespace LiteCharms.Features.Extensions;
public static class Postgres
{
public const string SchedulerDbConfigName = "PostgresScheduler";
public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration)
{
services.AddPooledDbContextFactory<ShopDbContext>(options =>
options.UseNpgsql(configuration.GetConnectionString("PostgresShop")));
return services;
}
}
+7 -13
View File
@@ -1,17 +1,15 @@
using LiteCharms.Features.Quartz;
using LiteCharms.Features.Quartz.Abstractions;
using static LiteCharms.Features.Extensions.Postgres;
namespace LiteCharms.Features.Extensions;
public static class Quartz
{
public const string TechShopSchedulerName = "tech-shop";
public const string MidrandShopSchedulerName = "midrand-shop";
private const string databaseConfigName = "PostgresScheduler";
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(SchedulerDbConfigName);
var connectionString = configuration.GetConnectionString(databaseConfigName);
services.ConfigureCommon();
@@ -36,7 +34,7 @@ public static class Quartz
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2);
});
});
});
@@ -46,12 +44,10 @@ public static class Quartz
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(SchedulerDbConfigName);
var connectionString = configuration.GetConnectionString(databaseConfigName);
services.ConfigureCommon();
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
services.AddQuartz(config =>
{
config.SchedulerName = schedulerName;
@@ -64,8 +60,6 @@ public static class Quartz
config.UseDefaultThreadPool(options => options.MaxConcurrency = 1);
config.UseTimeZoneConverter();
config.SetProperty("quartz.jobStore.misfireThreshold", TimeSpan.FromMinutes(2).TotalMilliseconds.ToString());
config.UsePersistentStore(storage =>
{
storage.PerformSchemaValidation = false;
@@ -78,7 +72,7 @@ public static class Quartz
storage.UseClustering(cluster =>
{
cluster.CheckinInterval = TimeSpan.FromSeconds(30);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(90);
cluster.CheckinMisfireThreshold = TimeSpan.FromSeconds(2);
});
});
});
@@ -92,14 +86,14 @@ public static class Quartz
{
options.Scheduling.IgnoreDuplicates = true;
options.Scheduling.OverWriteExistingData = true;
options["quartz.plugin.jobHistory.type"] = "Quartz.Plugin.History.LoggingJobHistoryPlugin, Quartz.Plugins";
options["quartz.plugin.triggerHistory.type"] = "Quartz.Plugin.History.LoggingTriggerHistoryPlugin, Quartz.Plugins";
});
services.AddTransient<RetryJobListener>();
services.AddTransient<IJobOrchestrator, JobOrchestrator>();
services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);
return services;
}
}
-64
View File
@@ -1,64 +0,0 @@
using LiteCharms.Features.S3;
using LiteCharms.Features.S3.Abstractions;
using static LiteCharms.Features.S3.Constants;
namespace LiteCharms.Features.Extensions;
public static class S3
{
public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration)
{
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value))
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopBucketName, (provider, client) =>
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopS3SettingsSection}:AccessKey").Value,
configuration.GetSection($"{BookshopS3SettingsSection}:SecretKey").Value),
new AmazonS3Config
{
ServiceURL = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value,
AuthenticationRegion = configuration.GetSection($"{BookshopS3SettingsSection}:Region").Value,
ForcePathStyle = true,
EndpointDiscoveryEnabled = true,
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
}));
services.AddKeyedScoped<IS3Service, BookshopS3Service>(BookshopBucketName);
}
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value))
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopInvoicesBucketName, (provider, client) =>
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:AccessKey").Value,
configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:SecretKey").Value),
new AmazonS3Config
{
ServiceURL = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value,
AuthenticationRegion = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:Region").Value,
ForcePathStyle = true,
EndpointDiscoveryEnabled = true,
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
}));
services.AddKeyedScoped<IS3Service, BookshopInvoicesS3Service>(BookshopInvoicesBucketName);
}
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value))
{
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopQuotesBucketName, (provider, client) =>
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:AccessKey").Value,
configuration.GetSection($"{BookshopQuotesS3SettingsSection}:SecretKey").Value),
new AmazonS3Config
{
ServiceURL = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value,
AuthenticationRegion = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:Region").Value,
ForcePathStyle = true,
EndpointDiscoveryEnabled = true,
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
}));
services.AddKeyedScoped<IS3Service, BookshopQuotesS3Service>(BookshopQuotesBucketName);
}
return services;
}
}
+27
View File
@@ -0,0 +1,27 @@
using LiteCharms.Features.Shop.CartPackages;
using LiteCharms.Features.Shop.Customers;
using LiteCharms.Features.Shop.Leads;
using LiteCharms.Features.Shop.Notifications;
using LiteCharms.Features.Shop.Orders;
using LiteCharms.Features.Shop.Products;
using LiteCharms.Features.Shop.Quotes;
using LiteCharms.Features.Shop.ShoppingCarts;
namespace LiteCharms.Features.Extensions;
public static class Shop
{
public static IServiceCollection AddShopServices(this IServiceCollection services)
{
services.AddSingleton<PackageService>()
.AddSingleton<LeadService>()
.AddSingleton<NotificationService>()
.AddSingleton<OrderService>()
.AddSingleton<ProductService>()
.AddSingleton<QuoteService>()
.AddSingleton<ShoppingCartService>()
.AddSingleton<CustomerService>();
return services;
}
}
@@ -1,10 +1,8 @@
using static LiteCharms.Features.TechShop.Extensions.Postgres;
namespace LiteCharms.Features.HealthChecks;
namespace LiteCharms.Features.TechShop.HealthChecks;
public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck
public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
{
private readonly string connectionString = configuration.GetConnectionString(TechShopDbConfigName)!;
private readonly string connectionString = configuration.GetConnectionString("PostgresShop")!;
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
@@ -18,11 +16,11 @@ public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthChec
await command.ExecuteScalarAsync(cancellationToken);
return HealthCheckResult.Healthy($"{TechShopDbConfigName} is responsive.");
return HealthCheckResult.Healthy("PostgreSQL is responsive.");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy($"{TechShopDbConfigName} is unreachable.", ex);
return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex);
}
}
}
@@ -0,0 +1,23 @@
namespace LiteCharms.Features.HealthChecks;
public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
{
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var scheduler = await schedulerFactory.GetScheduler(cancellationToken);
if (!scheduler.IsStarted)
return HealthCheckResult.Unhealthy("Quartz scheduler is not running");
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
return HealthCheckResult.Healthy("Quartz scheduler is ready");
}
catch (SchedulerException)
{
return HealthCheckResult.Unhealthy("Quartz scheduler cannot connect to the store");
}
}
}
+8 -11
View File
@@ -128,17 +128,6 @@
<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" />
<!-- global Usings -->
<Using Include="Amazon.S3" />
<Using Include="Amazon.S3.Model" />
<Using Include="Amazon.Runtime" />
</ItemGroup>
<!-- Shared Usings -->
<ItemGroup>
@@ -155,5 +144,13 @@
<Using Include="Microsoft.Extensions.Options" />
<Using Include="Microsoft.Extensions.Logging" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Shop\Postgres\Migrations\" />
</ItemGroup>
</Project>
@@ -1,10 +0,0 @@
namespace LiteCharms.Features.Models;
public class PageReference
{
public string? Tag { get; set; }
public string? Description { get; set; }
public string? Url { get; set; }
}
@@ -1,18 +0,0 @@
namespace LiteCharms.Features.Models;
public class ProductFilter
{
public string? Name { get; set; }
public string? Title { get; set; }
public string? Category { get; set; }
public string? Manufacturer { get; set; }
public string? SerialNumber { get; set; }
public decimal MinPrice { get; set; }
public decimal MaxPrice { get; set; }
}
@@ -1,12 +0,0 @@
namespace LiteCharms.Features.Models;
public class ProductMetadata
{
public string? Manufacturer { get; set; }
public string? ManufactureDate { get; set; }
public string? CopyrightInfo { get; set; }
public string? SerialNumber { get; set; }
}
-13
View File
@@ -1,13 +0,0 @@
namespace LiteCharms.Features.Models;
public class SocialMedia
{
public SocialMediaTypes Type { get; set; }
public string? Name { get; set; }
public string? ImageUrl { get; set; }
public string? Url { get; set; }
}
@@ -1,5 +1,6 @@
using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Quartz.Abstractions;
using static LiteCharms.Features.Extensions.Timezones;
namespace LiteCharms.Features.Quartz;
@@ -46,13 +47,13 @@ public class JobOrchestrator(ISchedulerFactory schedulerFactory) : IJobOrchestra
.StoreDurably()
.Build();
var now = DateTime.UtcNow;
var now = SouthAfricanTimeZone.UtcNow();
var trigger = global::Quartz.TriggerBuilder.Create()
.WithIdentity(triggerKey)
.WithDescription($"Scheduled via Main Job at {now:g}")
.WithCronSchedule(cronExpression, cron => cron
.WithMisfireHandlingInstructionIgnoreMisfires())
.WithCronSchedule(cronExpression, cron => cron.InTimeZone(SouthAfricanTimeZone)
.WithMisfireHandlingInstructionFireAndProceed())
.StartAt(now)
.Build();
+4 -20
View File
@@ -1,5 +1,4 @@
using LiteCharms.Features.Abstractions;
using LiteCharms.Features.Mediator;
namespace LiteCharms.Features.Quartz;
@@ -10,28 +9,13 @@ public class MediatorJob<TNotification>(IMediator mediator) : IJob where TNotifi
{
var data = context.MergedJobDataMap["Payload"] as string;
if (string.IsNullOrWhiteSpace(data))
{
Trace.WriteLine("Job Payload missing, job ended");
return;
}
if (string.IsNullOrWhiteSpace(data)) return;
var notification = JsonSerializer.Deserialize<TNotification>(data);
if (notification is null)
{
Trace.WriteLine("Notification could not be JSon converted from data string, job ended");
if(notification is null) return;
return;
}
using var activity = MediatorTelemetry.Source.StartActivity($"Quartz: {typeof(TNotification).Name}");
activity?.SetTag("event.correlation_id", notification.CorrelationId);
await mediator.Publish(notification, context.CancellationToken);
Trace.WriteLine("Job published");
if(notification is TNotification)
await mediator.Publish(notification, context.CancellationToken);
}
}
@@ -1,7 +0,0 @@
namespace LiteCharms.Features.S3.Abstractions;
public interface IS3Service
{
Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
}

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