Compare commits
4
Commits
87da491ed6
...
20b747e89c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20b747e89c | ||
|
|
7136e4fc70 | ||
|
|
4a85d01d1a | ||
|
|
d55bf4f82f |
@@ -0,0 +1,3 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Abstractions;
|
||||||
|
|
||||||
|
public interface IService;
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 new virtual Product? Product { get; set; }
|
||||||
|
|
||||||
|
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.Product)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(f => f.ProductId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using LiteCharms.Features.MidrandBooks.Products.Models;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
||||||
|
|
||||||
|
public class AuthorBook
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public long AuthorId { get; set; }
|
||||||
|
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
|
||||||
|
public int Rating { get; set; }
|
||||||
|
|
||||||
|
public int Ranking { get; set; }
|
||||||
|
|
||||||
|
public Product? Product { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,408 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||||
|
|
||||||
|
[EntityTypeConfiguration<AddressConfiguration, Address>]
|
||||||
|
public class Address : Models.Address
|
||||||
|
{
|
||||||
|
public virtual Customer? Customer { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||||
|
|
||||||
|
public class AddressConfiguration : IEntityTypeConfiguration<Address>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Address> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Addresses");
|
||||||
|
|
||||||
|
builder.HasKey(a => a.Id);
|
||||||
|
builder.Property(a => a.CustomerId).IsRequired();
|
||||||
|
builder.Property(a => a.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||||
|
builder.Property(a => a.UpdatedAt).HasDefaultValueSql("now()");
|
||||||
|
builder.Property(a => a.Name).IsRequired();
|
||||||
|
builder.Property(a => a.Type).IsRequired();
|
||||||
|
builder.Property(a => a.BuildingType).IsRequired();
|
||||||
|
builder.Property(a => a.Street).IsRequired();
|
||||||
|
builder.Property(a => a.City).IsRequired();
|
||||||
|
builder.Property(a => a.State).IsRequired();
|
||||||
|
builder.Property(a => a.PostalCode).IsRequired();
|
||||||
|
builder.Property(a => a.Country).IsRequired();
|
||||||
|
builder.Property(a => a.IsPrimary).HasDefaultValue(false);
|
||||||
|
builder.Property(a => a.Enabled).HasDefaultValue(true);
|
||||||
|
|
||||||
|
builder.HasOne(a => a.Customer)
|
||||||
|
.WithMany(c => c.Addresses)
|
||||||
|
.HasForeignKey(a => a.CustomerId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||||
|
|
||||||
|
[EntityTypeConfiguration<ContactConfiguration, Contact>]
|
||||||
|
public class Contact : Models.Contact
|
||||||
|
{
|
||||||
|
public virtual Customer? Customer { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||||
|
|
||||||
|
public class ContactConfiguration : IEntityTypeConfiguration<Contact>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Contact> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Contacts");
|
||||||
|
|
||||||
|
builder.HasKey(c => c.Id);
|
||||||
|
builder.Property(c => c.CustomerId).IsRequired();
|
||||||
|
builder.Property(c => c.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||||
|
builder.Property(c => c.UpdatedAt).HasDefaultValueSql("now()");
|
||||||
|
builder.Property(c => c.Name).IsRequired();
|
||||||
|
builder.Property(c => c.LastName).IsRequired();
|
||||||
|
builder.Property(c => c.Type).IsRequired();
|
||||||
|
builder.Property(c => c.Phone).IsRequired();
|
||||||
|
builder.Property(c => c.Email).IsRequired();
|
||||||
|
builder.Property(c => c.IsPrimary).HasDefaultValue(false);
|
||||||
|
builder.Property(c => c.Enabled).HasDefaultValue(true);
|
||||||
|
|
||||||
|
builder.HasOne(c => c.Customer)
|
||||||
|
.WithMany(c => c.Contacts)
|
||||||
|
.HasForeignKey(c => c.CustomerId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||||
|
|
||||||
|
[EntityTypeConfiguration<CustomerConfiguration, Customer>]
|
||||||
|
public class Customer : Models.Customer
|
||||||
|
{
|
||||||
|
public virtual ICollection<Contact> Contacts { get; set; } = [];
|
||||||
|
|
||||||
|
public virtual ICollection<Address> Addresses { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||||
|
|
||||||
|
public class Address
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public long CustomerId { get; set; }
|
||||||
|
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public AddressType Type { get; set; }
|
||||||
|
|
||||||
|
public AddressBuildingTypes BuildingType { get; set; }
|
||||||
|
|
||||||
|
public string? Street { get; set; }
|
||||||
|
|
||||||
|
public string? City { get; set; }
|
||||||
|
|
||||||
|
public string? State { get; set; }
|
||||||
|
|
||||||
|
public string? PostalCode { get; set; }
|
||||||
|
|
||||||
|
public string? Country { get; set; }
|
||||||
|
|
||||||
|
public bool IsPrimary { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||||
|
|
||||||
|
public class Contact
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public long CustomerId { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public ContactTypes Type { get; set; }
|
||||||
|
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
|
public bool IsPrimary { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using LiteCharms.Features.Models;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||||
|
|
||||||
|
public class Customer
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public string? Company { get; set; }
|
||||||
|
|
||||||
|
public string? VatNumber { get; set; }
|
||||||
|
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
public string? Website { get; set; }
|
||||||
|
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
|
public SocialMedia[]? SocialMedia { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using LiteCharms.Features.Models;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||||
|
|
||||||
|
public record CreateCustomer
|
||||||
|
{
|
||||||
|
public string? Company { get; set; }
|
||||||
|
|
||||||
|
public string? VatNumber { get; set; }
|
||||||
|
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
public string? Website { get; set; }
|
||||||
|
|
||||||
|
public string? Phone { get; set; }
|
||||||
|
|
||||||
|
public 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;
|
||||||
@@ -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)
|
public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandShopDbConfigName);
|
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandBooksDbConfigName);
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@ namespace LiteCharms.Features.MidrandBooks.Extensions;
|
|||||||
|
|
||||||
public static class Postgres
|
public static class Postgres
|
||||||
{
|
{
|
||||||
public const string MidrandShopDbConfigName = "PostgresMidrandBooks";
|
public const string MidrandBooksDbConfigName = "PostgresMidrandBooks";
|
||||||
|
|
||||||
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddPooledDbContextFactory<MidrandShopDbContext>(options =>
|
services.AddPooledDbContextFactory<MidrandBooksDbContext>(options =>
|
||||||
options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)));
|
options.UseNpgsql(configuration.GetConnectionString(MidrandBooksDbConfigName)));
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace LiteCharms.Features.MidrandBooks.HealthChecks;
|
|||||||
|
|
||||||
public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck
|
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)
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -18,11 +18,11 @@ public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHea
|
|||||||
|
|
||||||
await command.ExecuteScalarAsync(cancellationToken);
|
await command.ExecuteScalarAsync(cancellationToken);
|
||||||
|
|
||||||
return HealthCheckResult.Healthy($"{MidrandShopDbConfigName} is responsive.");
|
return HealthCheckResult.Healthy($"{MidrandBooksDbConfigName} is responsive.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return HealthCheckResult.Unhealthy($"{MidrandShopDbConfigName} is unreachable.", ex);
|
return HealthCheckResult.Unhealthy($"{MidrandBooksDbConfigName} is unreachable.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,4 +163,8 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Order\Entities\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@@ -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,226 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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>();
|
||||||
|
}
|
||||||
+6
-6
@@ -2,20 +2,20 @@
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Postgres;
|
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()
|
var configuration = new ConfigurationBuilder()
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
.AddUserSecrets(typeof(MidrandShopDbContext).Assembly)
|
.AddUserSecrets(typeof(MidrandBooksDbContext).Assembly)
|
||||||
.AddJsonFile("appsettings.json")
|
.AddJsonFile("appsettings.json")
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<MidrandShopDbContext>();
|
var optionsBuilder = new DbContextOptionsBuilder<MidrandBooksDbContext>();
|
||||||
optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName));
|
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,32 @@
|
|||||||
|
using LiteCharms.Features.Models;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Products.Models;
|
||||||
|
|
||||||
|
public class Product
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public ProductTypes Type { get; set; }
|
||||||
|
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
public string[]? ThumbnailUrls { get; set; }
|
||||||
|
|
||||||
|
public string[]? Categories { get; set; }
|
||||||
|
|
||||||
|
public ProductMetadata? Metadata { get; set; }
|
||||||
|
|
||||||
|
public ProductPrice? Price { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Products.Models;
|
||||||
|
|
||||||
|
public class ProductPrice
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
public decimal Discount { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,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,256 @@
|
|||||||
|
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,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.Customers.Models;
|
||||||
using LiteCharms.Features.TechShop.Extensions;
|
using LiteCharms.Features.TechShop.Extensions;
|
||||||
using LiteCharms.Features.TechShop.Postgres;
|
using LiteCharms.Features.TechShop.Postgres;
|
||||||
|
|||||||
@@ -23,6 +23,11 @@
|
|||||||
<PackageTags>utility;dotnet</PackageTags>
|
<PackageTags>utility;dotnet</PackageTags>
|
||||||
<PackageIcon>icon.png</PackageIcon>
|
<PackageIcon>icon.png</PackageIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Addresses\**" />
|
||||||
|
<EmbeddedResource Remove="Addresses\**" />
|
||||||
|
<None Remove="Addresses\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\LICENSE" Pack="true" PackagePath="\" />
|
<None Include="..\LICENSE" Pack="true" PackagePath="\" />
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using LiteCharms.Features.TechShop;
|
namespace LiteCharms.Features.TechShop.Orders.Models;
|
||||||
|
|
||||||
namespace LiteCharms.Features.TechShop.Orders.Models;
|
|
||||||
|
|
||||||
public class Order
|
public class Order
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace LiteCharms.Features.TechShop.Products.Models;
|
using LiteCharms.Features.Models;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.TechShop.Products.Models;
|
||||||
|
|
||||||
public class Product
|
public class Product
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,101 @@
|
|||||||
namespace LiteCharms.Features;
|
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
|
public enum EmailStatuses : int
|
||||||
{
|
{
|
||||||
GeneralError = 0,
|
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
|
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