Created Author, Book, AuthorBook, Page and Product with Price
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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)
|
||||
{
|
||||
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>> PublishBookAsync(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().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()
|
||||
.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using LiteCharms.Features.MidrandBooks.Authors.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Pages.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Products.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
|
||||
public class AuthorBook : Models.AuthorBook
|
||||
{
|
||||
public virtual Author Author { get; set; } = new();
|
||||
|
||||
public virtual Product Book { get; set; } = new();
|
||||
|
||||
public virtual ICollection<BookPage> Pages { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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.Book)
|
||||
.WithMany()
|
||||
.HasForeignKey(f => f.ProductId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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 bool Enabled { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
using LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
using LiteCharms.Features.MidrandBooks.Products.Models;
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Authors;
|
||||
|
||||
public class AuthorService(IDbContextFactory<MidrandBooksDbContext> contextFactory)
|
||||
{
|
||||
public async ValueTask<Result<Product[]>> 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<Product[]>(new Error($"Author with ID {authorId} not found"));
|
||||
|
||||
var books = await context.Books.AsNoTracking()
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.Where(p => p.AuthorId == authorId)
|
||||
.Select(p => p.Book.ToModel())
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return books?.Length > 0
|
||||
? Result.Ok(books)
|
||||
: Result.Fail<Product[]>(new Error($"No books found for author with ID {authorId}"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail<Product[]>(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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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; } = [];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||
|
||||
public class Author
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public PublisherTypes PublisherType { get; set; }
|
||||
|
||||
public string? Company { get; set; }
|
||||
|
||||
public string? VatNumber { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? LastName { get; set; }
|
||||
|
||||
public string? Biography { get; set; }
|
||||
|
||||
public string? Email { get; set; }
|
||||
|
||||
public string? Website { get; set; }
|
||||
|
||||
public string? ImageUrl { get; set; }
|
||||
|
||||
public string? ThumbnailImageUrl { get; set; }
|
||||
|
||||
public SocialMedia[]? SocialMedia { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
namespace LiteCharms.Features.MidrandBooks;
|
||||
|
||||
public enum PublisherTypes : int
|
||||
{
|
||||
Individual = 0,
|
||||
Company = 1,
|
||||
Organization = 2,
|
||||
SelfPublished = 3,
|
||||
UniversityPress = 4,
|
||||
GovernmentAgency = 5,
|
||||
NonProfit = 6,
|
||||
Independent = 7
|
||||
}
|
||||
|
||||
public enum BookTypes : int
|
||||
{
|
||||
Fiction = 0,
|
||||
NonFiction = 1,
|
||||
Academic = 2,
|
||||
SelfHelp = 3,
|
||||
Biography = 4,
|
||||
Poetry = 5,
|
||||
Children = 6,
|
||||
YoungAdult = 7,
|
||||
ScienceFiction = 8,
|
||||
Fantasy = 9
|
||||
}
|
||||
|
||||
public enum BookContentTypes : int
|
||||
{
|
||||
Text = 0,
|
||||
Image = 1,
|
||||
Video = 2,
|
||||
Audio = 3,
|
||||
Interactive = 4,
|
||||
Markdown = 5,
|
||||
Html = 6,
|
||||
Json = 7,
|
||||
Yaml = 8
|
||||
}
|
||||
|
||||
public enum BookPageTypes : int
|
||||
{
|
||||
Cover = 0,
|
||||
Preface = 1,
|
||||
Introduction = 2,
|
||||
Content = 3,
|
||||
Closing = 4,
|
||||
Referencer = 5,
|
||||
Credits = 6,
|
||||
BackCover = 7
|
||||
}
|
||||
|
||||
public enum ProductTypes : int
|
||||
{
|
||||
Book = 1,
|
||||
Journal = 2,
|
||||
Magazine = 3,
|
||||
EBook = 4,
|
||||
Audiobook = 5,
|
||||
Accessory = 6
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public static class HealthChecks
|
||||
|
||||
public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services)
|
||||
{
|
||||
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandShopDbConfigName);
|
||||
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandBooksDbConfigName);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Products.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
public static class Mappers
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
@@ -4,12 +4,12 @@ namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||
|
||||
public static class Postgres
|
||||
{
|
||||
public const string MidrandShopDbConfigName = "PostgresMidrandBooks";
|
||||
public const string MidrandBooksDbConfigName = "PostgresMidrandBooks";
|
||||
|
||||
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddPooledDbContextFactory<MidrandShopDbContext>(options =>
|
||||
options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)));
|
||||
services.AddPooledDbContextFactory<MidrandBooksDbContext>(options =>
|
||||
options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName)));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace LiteCharms.Features.MidrandBooks.HealthChecks;
|
||||
|
||||
public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck
|
||||
{
|
||||
private readonly string connectionString = configuration.GetConnectionString(MidrandShopDbConfigName)!;
|
||||
private readonly string connectionString = configuration.GetConnectionString(MidrandBooksDbConfigName)!;
|
||||
|
||||
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -18,11 +18,11 @@ public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHea
|
||||
|
||||
await command.ExecuteScalarAsync(cancellationToken);
|
||||
|
||||
return HealthCheckResult.Healthy($"{MidrandShopDbConfigName} is responsive.");
|
||||
return HealthCheckResult.Healthy($"{MidrandBooksDbConfigName} is responsive.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HealthCheckResult.Unhealthy($"{MidrandShopDbConfigName} is unreachable.", ex);
|
||||
return HealthCheckResult.Unhealthy($"{MidrandBooksDbConfigName} is unreachable.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages.Entities;
|
||||
|
||||
[EntityTypeConfiguration<BookPageConfiguration, BookPage>]
|
||||
public class BookPage : Models.BookPage
|
||||
{
|
||||
public virtual AuthorBook Book { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||
|
||||
public class BookPage
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public long AuthorBookId { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public BookPageTypes Type { get; set; }
|
||||
|
||||
public BookContentTypes ContentType { get; set; }
|
||||
|
||||
public int Number { get; set; }
|
||||
|
||||
public byte[]? Content { get; set; }
|
||||
|
||||
public string[]? Notes { get; set; }
|
||||
|
||||
public PageReference[]? References { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||
|
||||
public class UpdateBookPage : CreateBookPage;
|
||||
@@ -0,0 +1,225 @@
|
||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Pages;
|
||||
|
||||
public class PagesService(IDbContextFactory<MidrandBooksDbContext> contextFactory)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Entities;
|
||||
using LiteCharms.Features.MidrandBooks.Authors.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>();
|
||||
}
|
||||
+6
-6
@@ -2,20 +2,20 @@
|
||||
|
||||
namespace LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory<MidrandShopDbContext>
|
||||
public class MidrandBooksDbContextFactory : IDesignTimeDbContextFactory<MidrandBooksDbContext>
|
||||
{
|
||||
public MidrandShopDbContext CreateDbContext(string[] args)
|
||||
public MidrandBooksDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddUserSecrets(typeof(MidrandShopDbContext).Assembly)
|
||||
.AddUserSecrets(typeof(MidrandBooksDbContext).Assembly)
|
||||
.AddJsonFile("appsettings.json")
|
||||
.AddEnvironmentVariables()
|
||||
.Build();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<MidrandShopDbContext>();
|
||||
optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName));
|
||||
var optionsBuilder = new DbContextOptionsBuilder<MidrandBooksDbContext>();
|
||||
optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName));
|
||||
|
||||
return new MidrandShopDbContext(optionsBuilder.Options);
|
||||
return new MidrandBooksDbContext(optionsBuilder.Options);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Postgres;
|
||||
|
||||
public class MidrandShopDbContext(DbContextOptions<MidrandShopDbContext> options) : DbContext(options)
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ProductConfiguration, Product>]
|
||||
public class Product : Models.Product
|
||||
{
|
||||
public virtual ICollection<ProductPrice> Prices { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
|
||||
|
||||
[EntityTypeConfiguration<ProductPriceConfiguration, ProductPrice>]
|
||||
public class ProductPrice : Models.ProductPrice
|
||||
{
|
||||
public virtual Product Product { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Products.Models;
|
||||
|
||||
public class CreateProductPrice
|
||||
{
|
||||
public long ProductId { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
public decimal Discount { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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 bool Enabled { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace LiteCharms.Features.MidrandBooks.Products.Models;
|
||||
|
||||
public class ProductPrice
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public long ProductId { get; set; }
|
||||
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
public decimal Discount { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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; }
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
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)
|
||||
{
|
||||
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(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()
|
||||
.OrderByDescending(p => p.CreatedAt)
|
||||
.ThenBy(p => p.UpdatedAt)
|
||||
.Where(p => p.CreatedAt >= fromDate && p.CreatedAt <= toDate)
|
||||
.Take(range.MaxRecords)
|
||||
.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,5 +1,4 @@
|
||||
using LiteCharms.Features.Extensions;
|
||||
using LiteCharms.Features.Models;
|
||||
using LiteCharms.Features.Models;
|
||||
using LiteCharms.Features.TechShop.Customers.Models;
|
||||
using LiteCharms.Features.TechShop.Extensions;
|
||||
using LiteCharms.Features.TechShop.Postgres;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace LiteCharms.Features.TechShop.Products.Models;
|
||||
using LiteCharms.Features.Models;
|
||||
|
||||
namespace LiteCharms.Features.TechShop.Products.Models;
|
||||
|
||||
public class Product
|
||||
{
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
namespace LiteCharms.Features;
|
||||
|
||||
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,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class PageReference
|
||||
{
|
||||
public string? Tag { get; set; }
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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
-1
@@ -1,4 +1,4 @@
|
||||
namespace LiteCharms.Features.TechShop.Products.Models;
|
||||
namespace LiteCharms.Features.Models;
|
||||
|
||||
public class ProductMetadata
|
||||
{
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
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; }
|
||||
}
|
||||
Reference in New Issue
Block a user