Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 2m43s
Build and Deploy to Production / build-and-deploy (push) Successful in 2m43s
Features merged: - ZarinPal payment gateway (sandbox mode) - Discount store improvements (100% discount auto-apply) - VAT breakdown on checkout - PaymentStatus in order responses (proto v0.0.179) - ExpirePendingOrdersService (background job for stale orders) - DeliveryStatus proper mapping + cancel on failed payment - Inventory management system - Various bug fixes Production config preserved: - DB: 45.149.79.127 / KBS - CmsBaseUrl: https://cms.kbs1.ir - FrontOfficeBaseUrl: https://foursat.kbs1.ir - Payment gateway: sandbox (will show 'درگاه فعال نمیباشد') - Production workflow unchanged
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.CreateBlogCategory;
|
||||
|
||||
public class CreateBlogCategoryCommand : IRequest<long>
|
||||
{
|
||||
public string Title { get; set; } = default!;
|
||||
public string Slug { get; set; } = default!;
|
||||
public string? Description { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.CreateBlogCategory;
|
||||
|
||||
public class CreateBlogCategoryCommandHandler : IRequestHandler<CreateBlogCategoryCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public CreateBlogCategoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(CreateBlogCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = new BlogCategory
|
||||
{
|
||||
Title = request.Title,
|
||||
Slug = request.Slug.ToLower(),
|
||||
Description = request.Description,
|
||||
IconName = request.IconName,
|
||||
SortOrder = request.SortOrder,
|
||||
IsActive = request.IsActive
|
||||
};
|
||||
|
||||
_context.BlogCategories.Add(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.CreateBlogCategory;
|
||||
|
||||
public class CreateBlogCategoryCommandValidator : AbstractValidator<CreateBlogCategoryCommand>
|
||||
{
|
||||
public CreateBlogCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان دستهبندی الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان دستهبندی حداکثر ۲۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty().WithMessage("اسلاگ الزامی است")
|
||||
.MaximumLength(200).WithMessage("اسلاگ حداکثر ۲۰۰ کاراکتر")
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("اسلاگ فقط شامل حروف کوچک، اعداد و خط تیره");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000).WithMessage("توضیحات حداکثر ۱۰۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.IconName)
|
||||
.MaximumLength(100).WithMessage("نام آیکون حداکثر ۱۰۰ کاراکتر");
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.DeleteBlogCategory;
|
||||
|
||||
public class DeleteBlogCategoryCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.DeleteBlogCategory;
|
||||
|
||||
public class DeleteBlogCategoryCommandHandler : IRequestHandler<DeleteBlogCategoryCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteBlogCategoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeleteBlogCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.BlogCategories.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
if (entity == null || entity.IsDeleted)
|
||||
throw new NotFoundException(nameof(BlogCategory), request.Id);
|
||||
|
||||
entity.IsDeleted = true;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.UpdateBlogCategory;
|
||||
|
||||
public class UpdateBlogCategoryCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = default!;
|
||||
public string Slug { get; set; } = default!;
|
||||
public string? Description { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.UpdateBlogCategory;
|
||||
|
||||
public class UpdateBlogCategoryCommandHandler : IRequestHandler<UpdateBlogCategoryCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public UpdateBlogCategoryCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(UpdateBlogCategoryCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.BlogCategories.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
if (entity == null || entity.IsDeleted)
|
||||
throw new NotFoundException(nameof(BlogCategory), request.Id);
|
||||
|
||||
entity.Title = request.Title;
|
||||
entity.Slug = request.Slug.ToLower();
|
||||
entity.Description = request.Description;
|
||||
entity.IconName = request.IconName;
|
||||
entity.SortOrder = request.SortOrder;
|
||||
entity.IsActive = request.IsActive;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Commands.UpdateBlogCategory;
|
||||
|
||||
public class UpdateBlogCategoryCommandValidator : AbstractValidator<UpdateBlogCategoryCommand>
|
||||
{
|
||||
public UpdateBlogCategoryCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.GreaterThan(0).WithMessage("شناسه دستهبندی نامعتبر است");
|
||||
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان دستهبندی الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان دستهبندی حداکثر ۲۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty().WithMessage("اسلاگ الزامی است")
|
||||
.MaximumLength(200).WithMessage("اسلاگ حداکثر ۲۰۰ کاراکتر")
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("اسلاگ فقط شامل حروف کوچک، اعداد و خط تیره");
|
||||
|
||||
RuleFor(x => x.Description)
|
||||
.MaximumLength(1000).WithMessage("توضیحات حداکثر ۱۰۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.IconName)
|
||||
.MaximumLength(100).WithMessage("نام آیکون حداکثر ۱۰۰ کاراکتر");
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetActiveBlogCategories;
|
||||
|
||||
public class GetActiveBlogCategoriesQuery : IRequest<List<BlogCategoryDto>>
|
||||
{
|
||||
}
|
||||
|
||||
public class GetActiveBlogCategoriesQueryHandler : IRequestHandler<GetActiveBlogCategoriesQuery, List<BlogCategoryDto>>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetActiveBlogCategoriesQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<BlogCategoryDto>> Handle(GetActiveBlogCategoriesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var categories = await _context.BlogCategories
|
||||
.Include(x => x.BlogPostCategories)
|
||||
.Where(x => !x.IsDeleted && x.IsActive)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.Title)
|
||||
.Select(x => new BlogCategoryDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Title = x.Title,
|
||||
Slug = x.Slug,
|
||||
Description = x.Description,
|
||||
IconName = x.IconName,
|
||||
SortOrder = x.SortOrder,
|
||||
IsActive = x.IsActive,
|
||||
PostCount = x.BlogPostCategories.Count,
|
||||
Created = x.Created,
|
||||
LastModified = x.LastModified
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return categories;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetAllBlogCategories;
|
||||
|
||||
public class GetAllBlogCategoriesQuery : IRequest<GetAllBlogCategoriesResponseDto>
|
||||
{
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 20;
|
||||
public string? SearchTerm { get; set; }
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetAllBlogCategories;
|
||||
|
||||
public class GetAllBlogCategoriesQueryHandler : IRequestHandler<GetAllBlogCategoriesQuery, GetAllBlogCategoriesResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllBlogCategoriesQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllBlogCategoriesResponseDto> Handle(GetAllBlogCategoriesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.BlogCategories
|
||||
.Include(x => x.BlogPostCategories)
|
||||
.Where(x => !x.IsDeleted);
|
||||
|
||||
if (!string.IsNullOrEmpty(request.SearchTerm))
|
||||
{
|
||||
var term = request.SearchTerm.ToLower();
|
||||
query = query.Where(x => x.Title.ToLower().Contains(term) || x.Slug.ToLower().Contains(term));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var categories = await query
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenBy(x => x.Title)
|
||||
.Skip((request.PageNumber - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(x => new BlogCategoryDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Title = x.Title,
|
||||
Slug = x.Slug,
|
||||
Description = x.Description,
|
||||
IconName = x.IconName,
|
||||
SortOrder = x.SortOrder,
|
||||
IsActive = x.IsActive,
|
||||
PostCount = x.BlogPostCategories.Count,
|
||||
Created = x.Created,
|
||||
LastModified = x.LastModified
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var metaData = new MetaData
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
PageSize = request.PageSize,
|
||||
CurrentPage = request.PageNumber,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
||||
HasNext = request.PageNumber < (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
||||
HasPrevious = request.PageNumber > 1
|
||||
};
|
||||
|
||||
return new GetAllBlogCategoriesResponseDto { MetaData = metaData, Models = categories };
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetAllBlogCategories;
|
||||
|
||||
public class GetAllBlogCategoriesResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; } = default!;
|
||||
public List<BlogCategoryDto> Models { get; set; } = new();
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
|
||||
public class BlogCategoryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = default!;
|
||||
public string Slug { get; set; } = default!;
|
||||
public string? Description { get; set; }
|
||||
public string? IconName { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public int PostCount { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
|
||||
public class GetBlogCategoryQuery : IRequest<BlogCategoryDto>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
|
||||
|
||||
public class GetBlogCategoryQueryHandler : IRequestHandler<GetBlogCategoryQuery, BlogCategoryDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetBlogCategoryQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<BlogCategoryDto> Handle(GetBlogCategoryQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.BlogCategories
|
||||
.Include(x => x.BlogPostCategories)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
throw new NotFoundException(nameof(BlogCategory), request.Id);
|
||||
|
||||
return new BlogCategoryDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
Title = entity.Title,
|
||||
Slug = entity.Slug,
|
||||
Description = entity.Description,
|
||||
IconName = entity.IconName,
|
||||
SortOrder = entity.SortOrder,
|
||||
IsActive = entity.IsActive,
|
||||
PostCount = entity.BlogPostCategories.Count,
|
||||
Created = entity.Created,
|
||||
LastModified = entity.LastModified
|
||||
};
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
|
||||
|
||||
public class ArchiveBlogPostCommand : IRequest<ArchiveBlogPostResult>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
public class ArchiveBlogPostResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
|
||||
|
||||
public class ArchiveBlogPostCommandHandler : IRequestHandler<ArchiveBlogPostCommand, ArchiveBlogPostResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<ArchiveBlogPostCommandHandler> _logger;
|
||||
|
||||
public ArchiveBlogPostCommandHandler(IApplicationDbContext context, ILogger<ArchiveBlogPostCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ArchiveBlogPostResult> Handle(ArchiveBlogPostCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||
|
||||
post.Status = BlogPostStatus.Archived;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Blog post archived. Id: {Id}, Title: {Title}", post.Id, post.Title);
|
||||
|
||||
return new ArchiveBlogPostResult { Success = true, Message = "مقاله آرشیو شد" };
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
|
||||
|
||||
/// <summary>
|
||||
/// دستور ایجاد مقاله جدید
|
||||
/// </summary>
|
||||
public class CreateBlogPostCommand : IRequest<long>
|
||||
{
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string? Summary { get; set; }
|
||||
public string HtmlContent { get; set; } = string.Empty;
|
||||
public string? FeaturedImagePath { get; set; }
|
||||
public string? FeaturedImageThumbnailPath { get; set; }
|
||||
public List<long> CategoryIds { get; set; } = new();
|
||||
public List<long> TagIds { get; set; } = new();
|
||||
public bool IsFeatured { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
// Image upload properties
|
||||
public byte[]? ImageFileBytes { get; set; }
|
||||
public string? ImageFileMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
|
||||
|
||||
public class CreateBlogPostCommandHandler : IRequestHandler<CreateBlogPostCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
private readonly IFileManager _fileManager;
|
||||
private readonly ILogger<CreateBlogPostCommandHandler> _logger;
|
||||
|
||||
public CreateBlogPostCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser,
|
||||
IFileManager fileManager,
|
||||
ILogger<CreateBlogPostCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
_fileManager = fileManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(CreateBlogPostCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var currentUserId = _currentUser.UserId;
|
||||
if (string.IsNullOrEmpty(currentUserId) || !long.TryParse(currentUserId, out var authorUserId))
|
||||
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||
|
||||
var post = new BlogPost
|
||||
{
|
||||
Title = request.Title.Trim(),
|
||||
Slug = string.IsNullOrWhiteSpace(request.Slug)
|
||||
? $"post-{Guid.NewGuid():N}".Substring(0, 20)
|
||||
: request.Slug.Trim().ToLower(),
|
||||
Summary = request.Summary?.Trim(),
|
||||
HtmlContent = request.HtmlContent,
|
||||
FeaturedImagePath = request.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath,
|
||||
Status = BlogPostStatus.Draft,
|
||||
AuthorUserId = authorUserId,
|
||||
IsFeatured = request.IsFeatured,
|
||||
SortOrder = request.SortOrder,
|
||||
ViewCount = 0
|
||||
};
|
||||
|
||||
// آپلود تصویر شاخص (اگر فایل ارسال شده باشد)
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
var result = await _fileManager.UploadImageAsync(
|
||||
"Images/BlogPosts",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
post.FeaturedImagePath = result.Main.Path;
|
||||
post.FeaturedImageThumbnailPath = result.Thumbnail.Path;
|
||||
}
|
||||
|
||||
_context.BlogPosts.Add(post);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// افزودن دستهبندیها
|
||||
foreach (var categoryId in request.CategoryIds)
|
||||
{
|
||||
_context.BlogPostCategories.Add(new BlogPostCategory
|
||||
{
|
||||
BlogPostId = post.Id,
|
||||
BlogCategoryId = categoryId
|
||||
});
|
||||
}
|
||||
|
||||
// افزودن تگها
|
||||
foreach (var tagId in request.TagIds)
|
||||
{
|
||||
_context.BlogPostTags.Add(new BlogPostTag
|
||||
{
|
||||
BlogPostId = post.Id,
|
||||
TagId = tagId
|
||||
});
|
||||
}
|
||||
|
||||
if (request.CategoryIds.Any() || request.TagIds.Any())
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"Blog post created. Id: {Id}, Title: {Title}, Author: {Author}",
|
||||
post.Id, post.Title, authorUserId);
|
||||
|
||||
return post.Id;
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
|
||||
|
||||
public class CreateBlogPostCommandValidator : AbstractValidator<CreateBlogPostCommand>
|
||||
{
|
||||
public CreateBlogPostCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان مقاله الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty().WithMessage("نشانی یکتا (slug) الزامی است")
|
||||
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد");
|
||||
|
||||
RuleFor(x => x.Summary)
|
||||
.MaximumLength(500).WithMessage("خلاصه نمیتواند بیشتر از 500 کاراکتر باشد")
|
||||
.When(x => !string.IsNullOrEmpty(x.Summary));
|
||||
|
||||
RuleFor(x => x.HtmlContent)
|
||||
.NotEmpty().WithMessage("محتوای مقاله الزامی است");
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.DeleteBlogPost;
|
||||
|
||||
public class DeleteBlogPostCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.DeleteBlogPost;
|
||||
|
||||
public class DeleteBlogPostCommandHandler : IRequestHandler<DeleteBlogPostCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<DeleteBlogPostCommandHandler> _logger;
|
||||
|
||||
public DeleteBlogPostCommandHandler(IApplicationDbContext context, ILogger<DeleteBlogPostCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeleteBlogPostCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||
|
||||
post.IsDeleted = true;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Blog post soft-deleted. Id: {Id}", post.Id);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.IncrementViewCount;
|
||||
|
||||
public class IncrementViewCountCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.IncrementViewCount;
|
||||
|
||||
public class IncrementViewCountCommandHandler : IRequestHandler<IncrementViewCountCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public IncrementViewCountCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(IncrementViewCountCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken);
|
||||
if (post != null)
|
||||
{
|
||||
post.ViewCount++;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
|
||||
|
||||
public class PublishBlogPostCommand : IRequest<PublishBlogPostResult>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
|
||||
public class PublishBlogPostResult
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
|
||||
|
||||
public class PublishBlogPostCommandHandler : IRequestHandler<PublishBlogPostCommand, PublishBlogPostResult>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<PublishBlogPostCommandHandler> _logger;
|
||||
|
||||
public PublishBlogPostCommandHandler(IApplicationDbContext context, ILogger<PublishBlogPostCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PublishBlogPostResult> Handle(PublishBlogPostCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||
|
||||
if (post.Status == BlogPostStatus.Published)
|
||||
return new PublishBlogPostResult { Success = false, Message = "مقاله قبلاً منتشر شده است" };
|
||||
|
||||
post.Status = BlogPostStatus.Published;
|
||||
post.PublishedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Blog post published. Id: {Id}, Title: {Title}", post.Id, post.Title);
|
||||
|
||||
return new PublishBlogPostResult
|
||||
{
|
||||
Success = true,
|
||||
Message = "مقاله با موفقیت منتشر شد",
|
||||
PublishedAt = post.PublishedAt
|
||||
};
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
|
||||
|
||||
public class UpdateBlogPostCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string? Summary { get; set; }
|
||||
public string HtmlContent { get; set; } = string.Empty;
|
||||
public string? FeaturedImagePath { get; set; }
|
||||
public string? FeaturedImageThumbnailPath { get; set; }
|
||||
public List<long> CategoryIds { get; set; } = new();
|
||||
public List<long> TagIds { get; set; } = new();
|
||||
public bool IsFeatured { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
// Image upload properties
|
||||
public byte[]? ImageFileBytes { get; set; }
|
||||
public string? ImageFileMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
|
||||
|
||||
public class UpdateBlogPostCommandHandler : IRequestHandler<UpdateBlogPostCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManager _fileManager;
|
||||
private readonly ILogger<UpdateBlogPostCommandHandler> _logger;
|
||||
|
||||
public UpdateBlogPostCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IFileManager fileManager,
|
||||
ILogger<UpdateBlogPostCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_fileManager = fileManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(UpdateBlogPostCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||
|
||||
post.Title = request.Title.Trim();
|
||||
// حفظ slug قبلی اگر مقدار جدید خالی باشد
|
||||
if (!string.IsNullOrWhiteSpace(request.Slug))
|
||||
post.Slug = request.Slug.Trim().ToLower();
|
||||
post.Summary = request.Summary?.Trim();
|
||||
post.HtmlContent = request.HtmlContent;
|
||||
post.FeaturedImagePath = request.FeaturedImagePath;
|
||||
post.FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath;
|
||||
post.IsFeatured = request.IsFeatured;
|
||||
post.SortOrder = request.SortOrder;
|
||||
|
||||
// آپلود تصویر شاخص (اگر فایل جدید ارسال شده باشد)
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
var result = await _fileManager.UploadImageAsync(
|
||||
"Images/BlogPosts",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
post.FeaturedImagePath = result.Main.Path;
|
||||
post.FeaturedImageThumbnailPath = result.Thumbnail.Path;
|
||||
}
|
||||
|
||||
// بروزرسانی دستهبندیها
|
||||
var existingCategories = await _context.BlogPostCategories
|
||||
.Where(x => x.BlogPostId == post.Id).ToListAsync(cancellationToken);
|
||||
_context.BlogPostCategories.RemoveRange(existingCategories);
|
||||
foreach (var categoryId in request.CategoryIds)
|
||||
{
|
||||
_context.BlogPostCategories.Add(new BlogPostCategory
|
||||
{
|
||||
BlogPostId = post.Id,
|
||||
BlogCategoryId = categoryId
|
||||
});
|
||||
}
|
||||
|
||||
// بروزرسانی تگها
|
||||
var existingTags = await _context.BlogPostTags
|
||||
.Where(x => x.BlogPostId == post.Id).ToListAsync(cancellationToken);
|
||||
_context.BlogPostTags.RemoveRange(existingTags);
|
||||
foreach (var tagId in request.TagIds)
|
||||
{
|
||||
_context.BlogPostTags.Add(new BlogPostTag
|
||||
{
|
||||
BlogPostId = post.Id,
|
||||
TagId = tagId
|
||||
});
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
_logger.LogInformation("Blog post updated. Id: {Id}, Title: {Title}", post.Id, post.Title);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
|
||||
|
||||
public class UpdateBlogPostCommandValidator : AbstractValidator<UpdateBlogPostCommand>
|
||||
{
|
||||
public UpdateBlogPostCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Id).GreaterThan(0).WithMessage("شناسه مقاله نامعتبر است");
|
||||
|
||||
RuleFor(x => x.Title)
|
||||
.NotEmpty().WithMessage("عنوان مقاله الزامی است")
|
||||
.MaximumLength(200).WithMessage("عنوان نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.Slug)
|
||||
.NotEmpty().WithMessage("نشانی یکتا (slug) الزامی است")
|
||||
.MaximumLength(200).WithMessage("نشانی نمیتواند بیشتر از 200 کاراکتر باشد")
|
||||
.Matches(@"^[a-z0-9\-]+$").WithMessage("نشانی فقط میتواند شامل حروف کوچک انگلیسی، اعداد و خط تیره باشد");
|
||||
|
||||
RuleFor(x => x.HtmlContent)
|
||||
.NotEmpty().WithMessage("محتوای مقاله الزامی است");
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
|
||||
|
||||
public class GetAllBlogPostsQuery : IRequest<GetAllBlogPostsResponseDto>
|
||||
{
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 10;
|
||||
public string? SortBy { get; set; }
|
||||
public string? SearchTerm { get; set; }
|
||||
public BlogPostStatus? Status { get; set; }
|
||||
public long? CategoryId { get; set; }
|
||||
public bool? IsFeatured { get; set; }
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
|
||||
|
||||
public class GetAllBlogPostsQueryHandler : IRequestHandler<GetAllBlogPostsQuery, GetAllBlogPostsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetAllBlogPostsQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllBlogPostsResponseDto> Handle(GetAllBlogPostsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.BlogPosts
|
||||
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||
.Where(x => !x.IsDeleted);
|
||||
|
||||
// فیلترها
|
||||
if (request.Status.HasValue)
|
||||
query = query.Where(x => x.Status == request.Status.Value);
|
||||
if (request.CategoryId.HasValue)
|
||||
query = query.Where(x => x.BlogPostCategories.Any(c => c.BlogCategoryId == request.CategoryId.Value));
|
||||
if (request.IsFeatured.HasValue)
|
||||
query = query.Where(x => x.IsFeatured == request.IsFeatured.Value);
|
||||
if (!string.IsNullOrEmpty(request.SearchTerm))
|
||||
{
|
||||
var term = request.SearchTerm.ToLower();
|
||||
query = query.Where(x => x.Title.ToLower().Contains(term) || (x.Summary != null && x.Summary.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
// مرتبسازی
|
||||
query = request.SortBy?.ToLower() switch
|
||||
{
|
||||
"title" => query.OrderBy(x => x.Title),
|
||||
"viewcount" => query.OrderByDescending(x => x.ViewCount),
|
||||
"publishedat" => query.OrderByDescending(x => x.PublishedAt),
|
||||
_ => query.OrderByDescending(x => x.Created)
|
||||
};
|
||||
|
||||
var posts = await query
|
||||
.Skip((request.PageNumber - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(x => new BlogPostListItemDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Title = x.Title,
|
||||
Slug = x.Slug,
|
||||
Summary = x.Summary,
|
||||
FeaturedImageThumbnailPath = x.FeaturedImageThumbnailPath,
|
||||
Status = (int)x.Status,
|
||||
StatusName = GetBlogPostQueryHandler.GetStatusName(x.Status),
|
||||
PublishedAt = x.PublishedAt,
|
||||
ViewCount = x.ViewCount,
|
||||
IsFeatured = x.IsFeatured,
|
||||
Created = x.Created,
|
||||
Categories = x.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||
{
|
||||
Id = c.BlogCategory.Id,
|
||||
Title = c.BlogCategory.Title,
|
||||
Slug = c.BlogCategory.Slug
|
||||
}).ToList()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var metaData = new MetaData
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
PageSize = request.PageSize,
|
||||
CurrentPage = request.PageNumber,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
||||
HasNext = request.PageNumber < (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
||||
HasPrevious = request.PageNumber > 1
|
||||
};
|
||||
|
||||
return new GetAllBlogPostsResponseDto { MetaData = metaData, Models = posts };
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
|
||||
|
||||
public class GetAllBlogPostsResponseDto
|
||||
{
|
||||
public MetaData MetaData { get; set; } = new();
|
||||
public List<BlogPostListItemDto> Models { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BlogPostListItemDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string? Summary { get; set; }
|
||||
public string? FeaturedImageThumbnailPath { get; set; }
|
||||
public int Status { get; set; }
|
||||
public string StatusName { get; set; } = string.Empty;
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public bool IsFeatured { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public List<BlogPostCategoryDto> Categories { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
|
||||
public class BlogPostDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
public string? Summary { get; set; }
|
||||
public string HtmlContent { get; set; } = string.Empty;
|
||||
public string? FeaturedImagePath { get; set; }
|
||||
public string? FeaturedImageThumbnailPath { get; set; }
|
||||
public BlogPostStatus Status { get; set; }
|
||||
public string StatusName { get; set; } = string.Empty;
|
||||
public DateTime? PublishedAt { get; set; }
|
||||
public int ViewCount { get; set; }
|
||||
public long AuthorUserId { get; set; }
|
||||
public bool IsFeatured { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public DateTime Created { get; set; }
|
||||
public DateTime? LastModified { get; set; }
|
||||
public List<BlogPostCategoryDto> Categories { get; set; } = new();
|
||||
public List<BlogPostTagDto> Tags { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BlogPostCategoryDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class BlogPostTagDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
|
||||
public class GetBlogPostQuery : IRequest<BlogPostDto>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
|
||||
public class GetBlogPostQueryHandler : IRequestHandler<GetBlogPostQuery, BlogPostDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetBlogPostQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<BlogPostDto> Handle(GetBlogPostQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.BlogPosts
|
||||
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||
.Include(x => x.BlogPostTags).ThenInclude(x => x.Tag)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.Id && !x.IsDeleted, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"مقاله با شناسه {request.Id} یافت نشد");
|
||||
|
||||
return new BlogPostDto
|
||||
{
|
||||
Id = post.Id,
|
||||
Title = post.Title,
|
||||
Slug = post.Slug,
|
||||
Summary = post.Summary,
|
||||
HtmlContent = post.HtmlContent,
|
||||
FeaturedImagePath = post.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = post.FeaturedImageThumbnailPath,
|
||||
Status = post.Status,
|
||||
StatusName = GetStatusName(post.Status),
|
||||
PublishedAt = post.PublishedAt,
|
||||
ViewCount = post.ViewCount,
|
||||
AuthorUserId = post.AuthorUserId,
|
||||
IsFeatured = post.IsFeatured,
|
||||
SortOrder = post.SortOrder,
|
||||
Created = post.Created,
|
||||
LastModified = post.LastModified,
|
||||
Categories = post.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||
{
|
||||
Id = c.BlogCategory.Id,
|
||||
Title = c.BlogCategory.Title,
|
||||
Slug = c.BlogCategory.Slug
|
||||
}).ToList(),
|
||||
Tags = post.BlogPostTags.Select(t => new BlogPostTagDto
|
||||
{
|
||||
Id = t.Tag.Id,
|
||||
Title = t.Tag.Title,
|
||||
Name = t.Tag.Name
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public static string GetStatusName(BlogPostStatus status) => status switch
|
||||
{
|
||||
BlogPostStatus.Draft => "پیشنویس",
|
||||
BlogPostStatus.Published => "منتشرشده",
|
||||
BlogPostStatus.Scheduled => "زمانبندیشده",
|
||||
BlogPostStatus.Archived => "آرشیو",
|
||||
_ => "نامشخص"
|
||||
};
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPostBySlug;
|
||||
|
||||
public class GetBlogPostBySlugQuery : IRequest<GetBlogPost.BlogPostDto>
|
||||
{
|
||||
public string Slug { get; set; } = string.Empty;
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPostBySlug;
|
||||
|
||||
public class GetBlogPostBySlugQueryHandler : IRequestHandler<GetBlogPostBySlugQuery, BlogPostDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetBlogPostBySlugQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<BlogPostDto> Handle(GetBlogPostBySlugQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = await _context.BlogPosts
|
||||
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||
.Include(x => x.BlogPostTags).ThenInclude(x => x.Tag)
|
||||
.FirstOrDefaultAsync(x => x.Slug == request.Slug && !x.IsDeleted, cancellationToken)
|
||||
?? throw new KeyNotFoundException($"مقاله با نشانی '{request.Slug}' یافت نشد");
|
||||
|
||||
return new BlogPostDto
|
||||
{
|
||||
Id = post.Id,
|
||||
Title = post.Title,
|
||||
Slug = post.Slug,
|
||||
Summary = post.Summary,
|
||||
HtmlContent = post.HtmlContent,
|
||||
FeaturedImagePath = post.FeaturedImagePath,
|
||||
FeaturedImageThumbnailPath = post.FeaturedImageThumbnailPath,
|
||||
Status = post.Status,
|
||||
StatusName = GetBlogPostQueryHandler.GetStatusName(post.Status),
|
||||
PublishedAt = post.PublishedAt,
|
||||
ViewCount = post.ViewCount,
|
||||
AuthorUserId = post.AuthorUserId,
|
||||
IsFeatured = post.IsFeatured,
|
||||
SortOrder = post.SortOrder,
|
||||
Created = post.Created,
|
||||
LastModified = post.LastModified,
|
||||
Categories = post.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||
{
|
||||
Id = c.BlogCategory.Id,
|
||||
Title = c.BlogCategory.Title,
|
||||
Slug = c.BlogCategory.Slug
|
||||
}).ToList(),
|
||||
Tags = post.BlogPostTags.Select(t => new BlogPostTagDto
|
||||
{
|
||||
Id = t.Tag.Id,
|
||||
Title = t.Tag.Title,
|
||||
Name = t.Tag.Name
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetFeaturedBlogPosts;
|
||||
|
||||
public class GetFeaturedBlogPostsQuery : IRequest<List<GetAllBlogPosts.BlogPostListItemDto>>
|
||||
{
|
||||
public int Count { get; set; } = 5;
|
||||
}
|
||||
|
||||
public class GetFeaturedBlogPostsQueryHandler : IRequestHandler<GetFeaturedBlogPostsQuery, List<GetAllBlogPosts.BlogPostListItemDto>>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetFeaturedBlogPostsQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<GetAllBlogPosts.BlogPostListItemDto>> Handle(GetFeaturedBlogPostsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var posts = await _context.BlogPosts
|
||||
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||
.Where(x => !x.IsDeleted && x.Status == BlogPostStatus.Published && x.IsFeatured)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.ThenByDescending(x => x.PublishedAt)
|
||||
.Take(request.Count)
|
||||
.Select(x => new GetAllBlogPosts.BlogPostListItemDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Title = x.Title,
|
||||
Slug = x.Slug,
|
||||
Summary = x.Summary,
|
||||
FeaturedImageThumbnailPath = x.FeaturedImageThumbnailPath,
|
||||
Status = (int)x.Status,
|
||||
StatusName = GetBlogPostQueryHandler.GetStatusName(x.Status),
|
||||
PublishedAt = x.PublishedAt,
|
||||
ViewCount = x.ViewCount,
|
||||
IsFeatured = x.IsFeatured,
|
||||
Created = x.Created,
|
||||
Categories = x.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||
{
|
||||
Id = c.BlogCategory.Id,
|
||||
Title = c.BlogCategory.Title,
|
||||
Slug = c.BlogCategory.Slug
|
||||
}).ToList()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return posts;
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostCQ.Queries.GetPublishedBlogPosts;
|
||||
|
||||
public class GetPublishedBlogPostsQuery : IRequest<GetAllBlogPosts.GetAllBlogPostsResponseDto>
|
||||
{
|
||||
public int PageNumber { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 10;
|
||||
public string? SearchTerm { get; set; }
|
||||
public long? CategoryId { get; set; }
|
||||
}
|
||||
|
||||
public class GetPublishedBlogPostsQueryHandler : IRequestHandler<GetPublishedBlogPostsQuery, GetAllBlogPosts.GetAllBlogPostsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetPublishedBlogPostsQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<GetAllBlogPosts.GetAllBlogPostsResponseDto> Handle(GetPublishedBlogPostsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = _context.BlogPosts
|
||||
.Include(x => x.BlogPostCategories).ThenInclude(x => x.BlogCategory)
|
||||
.Where(x => !x.IsDeleted && x.Status == BlogPostStatus.Published);
|
||||
|
||||
if (request.CategoryId.HasValue)
|
||||
query = query.Where(x => x.BlogPostCategories.Any(c => c.BlogCategoryId == request.CategoryId.Value));
|
||||
if (!string.IsNullOrEmpty(request.SearchTerm))
|
||||
{
|
||||
var term = request.SearchTerm.ToLower();
|
||||
query = query.Where(x => x.Title.ToLower().Contains(term) || (x.Summary != null && x.Summary.ToLower().Contains(term)));
|
||||
}
|
||||
|
||||
var totalCount = await query.CountAsync(cancellationToken);
|
||||
|
||||
var posts = await query
|
||||
.OrderByDescending(x => x.PublishedAt)
|
||||
.Skip((request.PageNumber - 1) * request.PageSize)
|
||||
.Take(request.PageSize)
|
||||
.Select(x => new GetAllBlogPosts.BlogPostListItemDto
|
||||
{
|
||||
Id = x.Id,
|
||||
Title = x.Title,
|
||||
Slug = x.Slug,
|
||||
Summary = x.Summary,
|
||||
FeaturedImageThumbnailPath = x.FeaturedImageThumbnailPath,
|
||||
Status = (int)x.Status,
|
||||
StatusName = GetBlogPostQueryHandler.GetStatusName(x.Status),
|
||||
PublishedAt = x.PublishedAt,
|
||||
ViewCount = x.ViewCount,
|
||||
IsFeatured = x.IsFeatured,
|
||||
Created = x.Created,
|
||||
Categories = x.BlogPostCategories.Select(c => new BlogPostCategoryDto
|
||||
{
|
||||
Id = c.BlogCategory.Id,
|
||||
Title = c.BlogCategory.Title,
|
||||
Slug = c.BlogCategory.Slug
|
||||
}).ToList()
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var metaData = new MetaData
|
||||
{
|
||||
TotalCount = totalCount,
|
||||
PageSize = request.PageSize,
|
||||
CurrentPage = request.PageNumber,
|
||||
TotalPage = (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
||||
HasNext = request.PageNumber < (int)Math.Ceiling(totalCount / (double)request.PageSize),
|
||||
HasPrevious = request.PageNumber > 1
|
||||
};
|
||||
|
||||
return new GetAllBlogPosts.GetAllBlogPostsResponseDto { MetaData = metaData, Models = posts };
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.AddBlogPostImage;
|
||||
|
||||
public class AddBlogPostImageCommand : IRequest<long>
|
||||
{
|
||||
public long BlogPostId { get; set; }
|
||||
public string ImagePath { get; set; } = default!;
|
||||
public string ThumbnailPath { get; set; } = default!;
|
||||
public string? AltText { get; set; }
|
||||
public string? Caption { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.AddBlogPostImage;
|
||||
|
||||
public class AddBlogPostImageCommandHandler : IRequestHandler<AddBlogPostImageCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public AddBlogPostImageCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(AddBlogPostImageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var blogPost = await _context.BlogPosts.FirstOrDefaultAsync(x => x.Id == request.BlogPostId && !x.IsDeleted, cancellationToken);
|
||||
if (blogPost == null)
|
||||
throw new NotFoundException(nameof(BlogPost), request.BlogPostId);
|
||||
|
||||
var entity = new BlogPostImage
|
||||
{
|
||||
BlogPostId = request.BlogPostId,
|
||||
ImagePath = request.ImagePath,
|
||||
ThumbnailPath = request.ThumbnailPath,
|
||||
AltText = request.AltText,
|
||||
Caption = request.Caption,
|
||||
SortOrder = request.SortOrder
|
||||
};
|
||||
|
||||
_context.BlogPostImages.Add(entity);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return entity.Id;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.AddBlogPostImage;
|
||||
|
||||
public class AddBlogPostImageCommandValidator : AbstractValidator<AddBlogPostImageCommand>
|
||||
{
|
||||
public AddBlogPostImageCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.BlogPostId)
|
||||
.GreaterThan(0).WithMessage("شناسه پست نامعتبر است");
|
||||
|
||||
RuleFor(x => x.ImagePath)
|
||||
.NotEmpty().WithMessage("مسیر تصویر الزامی است");
|
||||
|
||||
RuleFor(x => x.AltText)
|
||||
.MaximumLength(500).WithMessage("متن جایگزین حداکثر ۵۰۰ کاراکتر");
|
||||
|
||||
RuleFor(x => x.Caption)
|
||||
.MaximumLength(1000).WithMessage("عنوان تصویر حداکثر ۱۰۰۰ کاراکتر");
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.DeleteBlogPostImage;
|
||||
|
||||
public class DeleteBlogPostImageCommand : IRequest<Unit>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using CMSMicroservice.Application.Common.Exceptions;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.DeleteBlogPostImage;
|
||||
|
||||
public class DeleteBlogPostImageCommandHandler : IRequestHandler<DeleteBlogPostImageCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteBlogPostImageCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(DeleteBlogPostImageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var entity = await _context.BlogPostImages.FindAsync(new object[] { request.Id }, cancellationToken);
|
||||
if (entity == null || entity.IsDeleted)
|
||||
throw new NotFoundException(nameof(BlogPostImage), request.Id);
|
||||
|
||||
entity.IsDeleted = true;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.ReorderBlogPostImages;
|
||||
|
||||
public class ReorderBlogPostImagesCommand : IRequest<Unit>
|
||||
{
|
||||
public List<ImageSortItem> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ImageSortItem
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Commands.ReorderBlogPostImages;
|
||||
|
||||
public class ReorderBlogPostImagesCommandHandler : IRequestHandler<ReorderBlogPostImagesCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public ReorderBlogPostImagesCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(ReorderBlogPostImagesCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var ids = request.Items.Select(x => x.Id).ToList();
|
||||
var images = await _context.BlogPostImages
|
||||
.Where(x => ids.Contains(x.Id) && !x.IsDeleted)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var item in request.Items)
|
||||
{
|
||||
var image = images.FirstOrDefault(x => x.Id == item.Id);
|
||||
if (image != null)
|
||||
image.SortOrder = item.SortOrder;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Unit.Value;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Queries.GetBlogPostImages;
|
||||
|
||||
public class GetBlogPostImagesQuery : IRequest<List<BlogPostImageDto>>
|
||||
{
|
||||
public long BlogPostId { get; set; }
|
||||
}
|
||||
|
||||
public class BlogPostImageDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long BlogPostId { get; set; }
|
||||
public string ImagePath { get; set; } = default!;
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public string? AltText { get; set; }
|
||||
public string? Caption { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.BlogPostImageCQ.Queries.GetBlogPostImages;
|
||||
|
||||
public class GetBlogPostImagesQueryHandler : IRequestHandler<GetBlogPostImagesQuery, List<BlogPostImageDto>>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public GetBlogPostImagesQueryHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<List<BlogPostImageDto>> Handle(GetBlogPostImagesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var images = await _context.BlogPostImages
|
||||
.Where(x => x.BlogPostId == request.BlogPostId && !x.IsDeleted)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.Select(x => new BlogPostImageDto
|
||||
{
|
||||
Id = x.Id,
|
||||
BlogPostId = x.BlogPostId,
|
||||
ImagePath = x.ImagePath,
|
||||
ThumbnailPath = x.ThumbnailPath,
|
||||
AltText = x.AltText,
|
||||
Caption = x.Caption,
|
||||
SortOrder = x.SortOrder
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return images;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||
|
||||
+12
-1
@@ -31,7 +31,18 @@ public class GetAllCategoryByFilterQueryHandler : IRequestHandler<GetAllCategory
|
||||
{
|
||||
MetaData = await query.GetMetaData(request.PaginationState, cancellationToken),
|
||||
Models = await query.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.ProjectToType<GetAllCategoryByFilterResponseModel>().ToListAsync(cancellationToken)
|
||||
.Select(x => new GetAllCategoryByFilterResponseModel
|
||||
{
|
||||
Id = x.Id,
|
||||
Name = x.Name,
|
||||
Title = x.Title,
|
||||
Description = x.Description,
|
||||
ImagePath = x.ImagePath,
|
||||
ParentId = x.ParentId,
|
||||
IsActive = x.IsActive,
|
||||
SortOrder = x.SortOrder,
|
||||
ProductCount = x.ProductCategories.Count
|
||||
}).ToListAsync(cancellationToken)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -24,4 +24,6 @@ public class GetAllCategoryByFilterResponseDto
|
||||
public bool IsActive { get; set; }
|
||||
//ترتیب نمایش
|
||||
public int SortOrder { get; set; }
|
||||
//تعداد محصولات
|
||||
public int ProductCount { get; set; }
|
||||
}
|
||||
|
||||
+12
-4
@@ -53,20 +53,28 @@ public class AcceptClubMembershipContractCommandHandler
|
||||
AcceptClubMembershipContractCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// خواندن UserId از JWT (امنتر از دریافت از کلاینت)
|
||||
if (!long.TryParse(_currentUser.UserId, out var userId))
|
||||
return new AcceptClubMembershipContractResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = "کاربر احراز هویت نشده است"
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Processing club membership contract for UserId: {UserId}",
|
||||
request.UserId
|
||||
userId
|
||||
);
|
||||
|
||||
// 1. دریافت کاربر
|
||||
var user = await _context.Users
|
||||
.Include(u => u.ClubMembership)
|
||||
.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||
.FirstOrDefaultAsync(u => u.Id == userId, cancellationToken);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
_logger.LogWarning("User not found: {UserId}", request.UserId);
|
||||
throw new NotFoundException(nameof(User), request.UserId);
|
||||
_logger.LogWarning("User not found: {UserId}", userId);
|
||||
throw new NotFoundException(nameof(User), userId);
|
||||
}
|
||||
|
||||
// 2. بررسی خرید پکیج
|
||||
|
||||
-4
@@ -4,10 +4,6 @@ public class AcceptClubMembershipContractCommandValidator : AbstractValidator<Ac
|
||||
{
|
||||
public AcceptClubMembershipContractCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.UserId)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("شناسه کاربر الزامی است");
|
||||
|
||||
RuleFor(x => x.OtpCode)
|
||||
.NotEmpty()
|
||||
.WithMessage("کد تایید الزامی است")
|
||||
|
||||
+25
-1
@@ -3,14 +3,18 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership
|
||||
public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQuery, ClubMembershipDto?>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ILogger<GetClubMembershipQueryHandler> _logger;
|
||||
|
||||
public GetClubMembershipQueryHandler(IApplicationDbContext context)
|
||||
public GetClubMembershipQueryHandler(IApplicationDbContext context, ILogger<GetClubMembershipQueryHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ClubMembershipDto?> Handle(GetClubMembershipQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("GetClubMembership called for UserId: {UserId}", request.UserId);
|
||||
|
||||
var membership = await _context.ClubMemberships
|
||||
.AsNoTracking()
|
||||
.Where(x => x.UserId == request.UserId)
|
||||
@@ -27,6 +31,26 @@ public class GetClubMembershipQueryHandler : IRequestHandler<GetClubMembershipQu
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
// اگر کاربر عضویت نداره، یک DTO با وضعیت غیرفعال برگردون
|
||||
if (membership == null)
|
||||
{
|
||||
_logger.LogInformation("No membership found for UserId: {UserId}, returning inactive status", request.UserId);
|
||||
return new ClubMembershipDto
|
||||
{
|
||||
Id = 0,
|
||||
UserId = request.UserId,
|
||||
IsActive = false,
|
||||
ActivatedAt = null,
|
||||
InitialContribution = 0,
|
||||
TotalEarned = 0,
|
||||
Created = DateTimeOffset.UtcNow,
|
||||
LastModified = null
|
||||
};
|
||||
}
|
||||
|
||||
_logger.LogInformation("Membership found for UserId: {UserId}, IsActive: {IsActive}, Id: {Id}",
|
||||
request.UserId, membership.IsActive, membership.Id);
|
||||
|
||||
return membership;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -6,14 +6,14 @@ namespace CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
|
||||
public record GetAllWeeklyPoolsQuery : IRequest<GetAllWeeklyPoolsResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// از هفته (فیلتر اختیاری)
|
||||
/// از هفته — WeekDefinitionId (فیلتر اختیاری)
|
||||
/// </summary>
|
||||
public int? FromWeekOrder { get; init; }
|
||||
public long? FromWeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// تا هفته (فیلتر اختیاری)
|
||||
/// تا هفته — WeekDefinitionId (فیلتر اختیاری)
|
||||
/// </summary>
|
||||
public int? ToWeekOrder { get; init; }
|
||||
public long? ToWeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فقط Pool های محاسبه شده
|
||||
|
||||
+4
-4
@@ -20,14 +20,14 @@ public class GetAllWeeklyPoolsQueryHandler : IRequestHandler<GetAllWeeklyPoolsQu
|
||||
.AsNoTracking();
|
||||
|
||||
// Apply filters
|
||||
if (request.FromWeekOrder!=null)
|
||||
if (request.FromWeekDefinitionId != null)
|
||||
{
|
||||
query = query.Where(x => x.WeekDefinition.WeekOrder>=request.FromWeekOrder );
|
||||
query = query.Where(x => x.WeekDefinitionId >= request.FromWeekDefinitionId);
|
||||
}
|
||||
|
||||
if (request.ToWeekOrder!=null)
|
||||
if (request.ToWeekDefinitionId != null)
|
||||
{
|
||||
query = query.Where(x =>x.WeekDefinition.WeekOrder<= request.ToWeekOrder);
|
||||
query = query.Where(x => x.WeekDefinitionId <= request.ToWeekDefinitionId);
|
||||
}
|
||||
|
||||
if (request.OnlyCalculated.HasValue && request.OnlyCalculated.Value)
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت پرداختهای کمیسیون کاربر جاری (از JWT)
|
||||
/// </summary>
|
||||
public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// فیلتر وضعیت
|
||||
/// </summary>
|
||||
public CommissionPayoutStatus? Status { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// شماره هفته (اختیاری)
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination
|
||||
/// </summary>
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Extensions;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissionPayoutsQuery, GetMyCommissionPayoutsResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public GetMyCommissionPayoutsQueryHandler(
|
||||
IApplicationDbContext context,
|
||||
ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<GetMyCommissionPayoutsResponseDto> Handle(GetMyCommissionPayoutsQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// دریافت UserId از JWT (فقط برای Customer API)
|
||||
if (!long.TryParse(_currentUser.UserId, out var userId))
|
||||
{
|
||||
throw new UnauthorizedAccessException("User not authenticated");
|
||||
}
|
||||
|
||||
var query = _context.UserCommissionPayouts
|
||||
.Include(x => x.WeekDefinition)
|
||||
.Where(x => x.UserId == userId)
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
// فیلترها
|
||||
if (request.Status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Status == request.Status.Value);
|
||||
}
|
||||
|
||||
if (request.WeekDefinitionId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.WeekDefinitionId == request.WeekDefinitionId.Value);
|
||||
}
|
||||
|
||||
// مرتبسازی: جدیدترین اول
|
||||
query = query.OrderByDescending(x => x.Created);
|
||||
|
||||
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
|
||||
|
||||
var models = await query
|
||||
.PaginatedListAsync(paginationState: request.PaginationState)
|
||||
.Select(x => new GetMyCommissionPayoutsResponseModel
|
||||
{
|
||||
Id = x.Id,
|
||||
WeekDefinitionId = x.WeekDefinitionId,
|
||||
WeekDisplayName = x.WeekDefinition != null ? x.WeekDefinition.DisplayName : "",
|
||||
BalancesEarned = x.BalancesEarned,
|
||||
TotalAmount = x.TotalAmount,
|
||||
AmountFormatted = x.TotalAmount.ToString("N0") + " تومان",
|
||||
Status = x.Status,
|
||||
CalculatedDate = x.PaidAt ?? (DateTime?)x.Created,
|
||||
DatePersian = ""
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new GetMyCommissionPayoutsResponseDto
|
||||
{
|
||||
MetaData = meta,
|
||||
Models = models
|
||||
};
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
public class GetMyCommissionPayoutsQueryValidator : AbstractValidator<GetMyCommissionPayoutsQuery>
|
||||
{
|
||||
public GetMyCommissionPayoutsQueryValidator()
|
||||
{
|
||||
RuleFor(x => x.PaginationState)
|
||||
.NotNull()
|
||||
.WithMessage("Pagination state is required");
|
||||
|
||||
When(x => x.PaginationState != null, () =>
|
||||
{
|
||||
RuleFor(x => x.PaginationState!.PageNumber)
|
||||
.GreaterThan(0)
|
||||
.WithMessage("Page number must be greater than 0");
|
||||
|
||||
RuleFor(x => x.PaginationState!.PageSize)
|
||||
.GreaterThan(0)
|
||||
.LessThanOrEqualTo(100)
|
||||
.WithMessage("Page size must be between 1 and 100");
|
||||
});
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
using CMSMicroservice.Application.Common.Models;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
|
||||
|
||||
public class GetMyCommissionPayoutsResponseDto
|
||||
{
|
||||
public MetaData? MetaData { get; set; }
|
||||
public List<GetMyCommissionPayoutsResponseModel> Models { get; set; } = new();
|
||||
}
|
||||
|
||||
public class GetMyCommissionPayoutsResponseModel
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long WeekDefinitionId { get; set; }
|
||||
public string WeekDisplayName { get; set; } = string.Empty;
|
||||
public int BalancesEarned { get; set; }
|
||||
public long TotalAmount { get; set; }
|
||||
public string AmountFormatted { get; set; } = string.Empty;
|
||||
public CommissionPayoutStatus Status { get; set; }
|
||||
public DateTime? CalculatedDate { get; set; }
|
||||
public string DatePersian { get; set; } = string.Empty;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت تعادلهای هفتگی کاربر جاری (از JWT)
|
||||
/// </summary>
|
||||
public record GetMyWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه تعریف هفته (اختیاری)
|
||||
/// </summary>
|
||||
public long? WeekDefinitionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// فقط موارد Expired نشده؟
|
||||
/// </summary>
|
||||
public bool OnlyActive { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Pagination
|
||||
/// </summary>
|
||||
public PaginationState? PaginationState { get; init; }
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
|
||||
|
||||
namespace CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای دریافت تعادلهای هفتگی کاربر جاری
|
||||
/// </summary>
|
||||
public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalancesQuery, GetUserWeeklyBalancesResponseDto>
|
||||
{
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<GetMyWeeklyBalancesQueryHandler> _logger;
|
||||
|
||||
public GetMyWeeklyBalancesQueryHandler(
|
||||
ICurrentUserService currentUserService,
|
||||
IMediator mediator,
|
||||
ILogger<GetMyWeeklyBalancesQueryHandler> logger)
|
||||
{
|
||||
_currentUserService = currentUserService;
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<GetUserWeeklyBalancesResponseDto> Handle(GetMyWeeklyBalancesQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// دریافت UserId از JWT
|
||||
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
|
||||
{
|
||||
_logger.LogWarning("GetMyWeeklyBalances called without valid user authentication");
|
||||
throw new UnauthorizedAccessException("کاربر احراز هویت نشده است");
|
||||
}
|
||||
|
||||
_logger.LogInformation("GetMyWeeklyBalances for UserId: {UserId}, WeekDefinitionId: {WeekDefinitionId}",
|
||||
userId, request.WeekDefinitionId);
|
||||
|
||||
// فراخوانی GetUserWeeklyBalancesQuery با UserId از JWT
|
||||
var query = new GetUserWeeklyBalancesQuery
|
||||
{
|
||||
UserId = userId,
|
||||
WeekDefinitionId = request.WeekDefinitionId,
|
||||
OnlyActive = request.OnlyActive,
|
||||
PaginationState = request.PaginationState
|
||||
};
|
||||
|
||||
return await _mediator.Send(query, cancellationToken);
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -21,10 +21,14 @@ public class GetUserCommissionPayoutsQueryHandler : IRequestHandler<GetUserCommi
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
// فیلترها
|
||||
if (request.UserId.HasValue)
|
||||
// UserId > 0 → filter by that user
|
||||
// UserId == 0 or null → show ALL users (admin mode)
|
||||
// Customer endpoints resolve UserId from JWT before calling this handler
|
||||
long? userId = request.UserId;
|
||||
|
||||
if (userId.HasValue && userId.Value > 0)
|
||||
{
|
||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||
query = query.Where(x => x.UserId == userId.Value);
|
||||
}
|
||||
|
||||
if (request.Status.HasValue)
|
||||
|
||||
+7
-3
@@ -21,10 +21,14 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
// فیلترها
|
||||
if (request.UserId.HasValue)
|
||||
// UserId > 0 → filter by that user
|
||||
// UserId == 0 or null → show ALL users (admin mode)
|
||||
// Customer endpoints resolve UserId from JWT before calling this handler
|
||||
long? userId = request.UserId;
|
||||
|
||||
if (userId.HasValue && userId.Value > 0)
|
||||
{
|
||||
query = query.Where(x => x.UserId == request.UserId.Value);
|
||||
query = query.Where(x => x.UserId == userId.Value);
|
||||
}
|
||||
|
||||
// فیلتر بر اساس WeekDefinitionId (روش ترجیحی)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace CMSMicroservice.Application.Common.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس بررسی مجوز کاربر بر اساس نقشهای JWT
|
||||
/// </summary>
|
||||
public interface IPermissionService
|
||||
{
|
||||
/// <summary>
|
||||
/// دریافت نقشهای کاربر فعلی از JWT Claims
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<string>> GetUserRolesAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// بررسی اینکه آیا کاربر فعلی مجوز مشخصی دارد
|
||||
/// </summary>
|
||||
Task<bool> HasPermissionAsync(string permission, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
namespace CMSMicroservice.Application.Common.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// ثوابت نام مجوزها — دستهبندی شده بر اساس حوزه
|
||||
/// </summary>
|
||||
public static class PermissionNames
|
||||
{
|
||||
// Dashboard
|
||||
public const string DashboardView = "dashboard.view";
|
||||
|
||||
// Orders
|
||||
public const string OrdersView = "orders.view";
|
||||
public const string OrdersCreate = "orders.create";
|
||||
public const string OrdersUpdate = "orders.update";
|
||||
public const string OrdersDelete = "orders.delete";
|
||||
public const string OrdersCancel = "orders.cancel";
|
||||
public const string OrdersApprove = "orders.approve";
|
||||
|
||||
// Products
|
||||
public const string ProductsView = "products.view";
|
||||
public const string ProductsCreate = "products.create";
|
||||
public const string ProductsUpdate = "products.update";
|
||||
public const string ProductsDelete = "products.delete";
|
||||
|
||||
// Users
|
||||
public const string UsersView = "users.view";
|
||||
public const string UsersUpdate = "users.update";
|
||||
public const string UsersDelete = "users.delete";
|
||||
|
||||
// Commission
|
||||
public const string CommissionView = "commission.view";
|
||||
public const string CommissionApproveWithdrawal = "commission.approve_withdrawal";
|
||||
|
||||
// Public Messages
|
||||
public const string PublicMessagesView = "publicmessages.view";
|
||||
public const string PublicMessagesCreate = "publicmessages.create";
|
||||
public const string PublicMessagesUpdate = "publicmessages.update";
|
||||
public const string PublicMessagesPublish = "publicmessages.publish";
|
||||
|
||||
// Manual Payments
|
||||
public const string ManualPaymentsView = "manualpayments.view";
|
||||
public const string ManualPaymentsCreate = "manualpayments.create";
|
||||
public const string ManualPaymentsApprove = "manualpayments.approve";
|
||||
|
||||
// Settings
|
||||
public const string SettingsView = "settings.view";
|
||||
public const string SettingsUpdate = "settings.update";
|
||||
public const string SettingsDelete = "settings.delete";
|
||||
public const string SettingsManageConfiguration = "settings.manage_configuration";
|
||||
public const string SettingsManageVat = "settings.manage_vat";
|
||||
|
||||
// Reports
|
||||
public const string ReportsView = "reports.view";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نام نقشها
|
||||
/// </summary>
|
||||
public static class RoleNames
|
||||
{
|
||||
public const string SuperAdmin = "Administrator";
|
||||
public const string Admin = "Admin";
|
||||
public const string Inspector = "Inspector";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// تنظیمات نقش→مجوز — ماتریس دسترسی
|
||||
/// </summary>
|
||||
public static class RolePermissionConfig
|
||||
{
|
||||
private static readonly Dictionary<string, HashSet<string>> RolePermissions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[RoleNames.SuperAdmin] = new(StringComparer.OrdinalIgnoreCase) { "*" }, // Full access
|
||||
|
||||
[RoleNames.Admin] = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
PermissionNames.DashboardView,
|
||||
PermissionNames.OrdersView,
|
||||
PermissionNames.OrdersCreate,
|
||||
PermissionNames.OrdersUpdate,
|
||||
PermissionNames.OrdersCancel,
|
||||
PermissionNames.ProductsView,
|
||||
PermissionNames.ProductsCreate,
|
||||
PermissionNames.ProductsUpdate,
|
||||
PermissionNames.ProductsDelete,
|
||||
PermissionNames.UsersView,
|
||||
PermissionNames.UsersUpdate,
|
||||
PermissionNames.CommissionView,
|
||||
PermissionNames.CommissionApproveWithdrawal,
|
||||
PermissionNames.PublicMessagesView,
|
||||
PermissionNames.PublicMessagesCreate,
|
||||
PermissionNames.PublicMessagesUpdate,
|
||||
PermissionNames.PublicMessagesPublish,
|
||||
PermissionNames.ManualPaymentsView,
|
||||
PermissionNames.ManualPaymentsCreate,
|
||||
PermissionNames.ReportsView
|
||||
},
|
||||
|
||||
[RoleNames.Inspector] = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
PermissionNames.DashboardView,
|
||||
PermissionNames.OrdersView,
|
||||
PermissionNames.UsersView,
|
||||
PermissionNames.CommissionView,
|
||||
PermissionNames.PublicMessagesView,
|
||||
PermissionNames.ReportsView
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// بررسی اینکه آیا نقش مشخصی مجوز خاصی دارد
|
||||
/// </summary>
|
||||
public static bool HasPermission(string role, string permission)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(role) || string.IsNullOrWhiteSpace(permission))
|
||||
return false;
|
||||
|
||||
if (!RolePermissions.TryGetValue(role, out var permissions))
|
||||
return false;
|
||||
|
||||
// Wildcard: SuperAdmin has full access
|
||||
if (permissions.Contains("*"))
|
||||
return true;
|
||||
|
||||
return permissions.Contains(permission);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace CMSMicroservice.Application.Common.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Attribute برای مشخص کردن مجوز لازم برای دسترسی به یک متد gRPC
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||
public sealed class RequiresPermissionAttribute : Attribute
|
||||
{
|
||||
public RequiresPermissionAttribute(string permission)
|
||||
{
|
||||
Permission = permission ?? throw new ArgumentNullException(nameof(permission));
|
||||
}
|
||||
|
||||
public string Permission { get; }
|
||||
}
|
||||
@@ -18,7 +18,10 @@ public class LoggingBehaviour<TRequest> : IRequestPreProcessor<TRequest> where T
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var userId = _currentUserService.UserId ?? string.Empty;
|
||||
_logger.LogInformation("Request: {Name} {@UserId} {@Request}",
|
||||
requestName, userId, request);
|
||||
var safeLog = request?.ToString() ?? "";
|
||||
if (safeLog.Length > 2000)
|
||||
safeLog = safeLog[..2000] + "... [TRUNCATED]";
|
||||
_logger.LogInformation("Request: {Name} {UserId} {Request}",
|
||||
requestName, userId, safeLog);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text.RegularExpressions;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Behaviours;
|
||||
|
||||
public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
public partial class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
private readonly Stopwatch _timer;
|
||||
private readonly ILogger<TRequest> _logger;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
[GeneratedRegex(@"ImageFile(?:Bytes|Mime|FileName)|ImageFile|File", RegexOptions.None)]
|
||||
private static partial Regex BinaryPropertyPattern();
|
||||
|
||||
public PerformanceBehaviour(ILogger<TRequest> logger, ICurrentUserService currentUserService)
|
||||
{
|
||||
_timer = new Stopwatch();
|
||||
@@ -33,11 +37,20 @@ public class PerformanceBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequ
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var userId = _currentUserService.UserId ?? string.Empty;
|
||||
var safeLog = SanitizeForLog(request);
|
||||
|
||||
_logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@Request}",
|
||||
requestName, elapsedMilliseconds, userId, request);
|
||||
_logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {UserId} {Request}",
|
||||
requestName, elapsedMilliseconds, userId, safeLog);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
private static string SanitizeForLog(TRequest request)
|
||||
{
|
||||
var text = request?.ToString() ?? "";
|
||||
if (text.Length > 2000)
|
||||
return text[..2000] + "... [TRUNCATED]";
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@ public class UnhandledExceptionBehaviour<TRequest, TResponse> : IPipelineBehavio
|
||||
catch (Exception ex)
|
||||
{
|
||||
var requestName = typeof(TRequest).Name;
|
||||
var safeLog = request?.ToString() ?? "";
|
||||
if (safeLog.Length > 2000)
|
||||
safeLog = safeLog[..2000] + "... [TRUNCATED]";
|
||||
|
||||
_logger.LogError(ex, "Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
|
||||
_logger.LogError(ex, "Request: Unhandled Exception for Request {Name} {Request}", requestName, safeLog);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
namespace CMSMicroservice.Application.Common.FileManager;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس مدیریت فایل — ذخیره روی دیسک، مسیر نسبی در دیتابیس
|
||||
/// موقع واکشی: خواندن از دیسک و تبدیل به base64 data-URI
|
||||
/// </summary>
|
||||
public interface IFileManager
|
||||
{
|
||||
/// <summary>
|
||||
/// آپلود یک فایل خام به دیسک
|
||||
/// </summary>
|
||||
/// <returns>نتیجه آپلود شامل مسیر نسبی فایل</returns>
|
||||
/// <exception cref="FileUploadException">در صورت خطای آپلود</exception>
|
||||
Task<UploadedFile> UploadAsync(
|
||||
string directory,
|
||||
byte[] fileBytes,
|
||||
string mime,
|
||||
string? fileName = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// آپلود تصویر با بهینهسازی خودکار + ساخت بندانگشتی
|
||||
/// </summary>
|
||||
/// <returns>نتیجه آپلود شامل تصویر اصلی و بندانگشتی (مسیرهای نسبی)</returns>
|
||||
/// <exception cref="FileUploadException">در صورت خطای آپلود</exception>
|
||||
Task<UploadedImage> UploadImageAsync(
|
||||
string directory,
|
||||
byte[] fileBytes,
|
||||
string mime,
|
||||
string? fileName = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// حذف فایل از دیسک
|
||||
/// </summary>
|
||||
Task DeleteAsync(long fileId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// خواندن فایل از دیسک و تبدیل به base64 data-URI
|
||||
/// اگر مسیر از قبل data: باشد، همان را برمیگرداند
|
||||
/// اگر فایل وجود نداشته باشد، رشته خالی برمیگرداند
|
||||
/// </summary>
|
||||
string ResolveImageUrl(string? path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// نتیجه آپلود فایل
|
||||
/// </summary>
|
||||
/// <param name="Id">شناسه فایل در FMS</param>
|
||||
/// <param name="Path">مسیر فایل ذخیرهشده (مثلاً /Images/Products/abc.jpg)</param>
|
||||
public sealed record UploadedFile(long Id, string Path);
|
||||
|
||||
/// <summary>
|
||||
/// نتیجه آپلود تصویر — شامل تصویر اصلی و بندانگشتی
|
||||
/// </summary>
|
||||
/// <param name="Main">تصویر اصلی بهینهشده</param>
|
||||
/// <param name="Thumbnail">تصویر بندانگشتی</param>
|
||||
public sealed record UploadedImage(UploadedFile Main, UploadedFile Thumbnail);
|
||||
|
||||
/// <summary>
|
||||
/// خطای آپلود فایل
|
||||
/// </summary>
|
||||
public class FileUploadException : Exception
|
||||
{
|
||||
public FileUploadException(string message) : base(message) { }
|
||||
public FileUploadException(string message, Exception inner) : base(message, inner) { }
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using CMSMicroservice.Domain.Entities.Blog;
|
||||
using CMSMicroservice.Domain.Entities.Content;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Entities.Order;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
@@ -32,6 +34,7 @@ public interface IApplicationDbContext
|
||||
DbSet<UserWallet> UserWallets { get; }
|
||||
DbSet<UserWalletChangeLog> UserWalletChangeLogs { get; }
|
||||
DbSet<ManualPayment> ManualPayments { get; }
|
||||
DbSet<PaymentTransaction> PaymentTransactions { get; }
|
||||
DbSet<PublicMessage> PublicMessages { get; }
|
||||
DbSet<ClubMembership> ClubMemberships { get; }
|
||||
DbSet<ClubMembershipHistory> ClubMembershipHistories { get; }
|
||||
@@ -51,6 +54,7 @@ public interface IApplicationDbContext
|
||||
DbSet<DiscountProduct> DiscountProducts { get; }
|
||||
DbSet<DiscountCategory> DiscountCategories { get; }
|
||||
DbSet<DiscountProductCategory> DiscountProductCategories { get; }
|
||||
DbSet<DiscountProductImage> DiscountProductImages { get; }
|
||||
DbSet<DiscountShoppingCart> DiscountShoppingCarts { get; }
|
||||
DbSet<DiscountOrder> DiscountOrders { get; }
|
||||
DbSet<DiscountOrderDetail> DiscountOrderDetails { get; }
|
||||
@@ -60,6 +64,22 @@ public interface IApplicationDbContext
|
||||
DbSet<State> States { get; }
|
||||
DbSet<City> Cities { get; }
|
||||
|
||||
// ============= Inventory Management =============
|
||||
DbSet<Warehouse> Warehouses { get; }
|
||||
DbSet<InventoryItem> InventoryItems { get; }
|
||||
DbSet<StockMovement> StockMovements { get; }
|
||||
|
||||
// ============= Blog =============
|
||||
DbSet<BlogPost> BlogPosts { get; }
|
||||
DbSet<BlogCategory> BlogCategories { get; }
|
||||
DbSet<BlogPostCategory> BlogPostCategories { get; }
|
||||
DbSet<BlogPostTag> BlogPostTags { get; }
|
||||
DbSet<BlogPostImage> BlogPostImages { get; }
|
||||
|
||||
// ============= Content Management =============
|
||||
DbSet<SitePage> SitePages { get; }
|
||||
DbSet<SitePageSection> SitePageSections { get; }
|
||||
|
||||
/// <summary>
|
||||
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس مدیریت موجودی - لایه بالاتر برای عملیات business
|
||||
/// این سرویس مسئول همگامسازی موجودی بین InventoryItem و Product.RemainingCount است
|
||||
/// </summary>
|
||||
public interface IInventoryService
|
||||
{
|
||||
#region Initialization
|
||||
|
||||
/// <summary>
|
||||
/// ایجاد رکورد موجودی برای محصول جدید
|
||||
/// این متد باید در CreateProductCommandHandler و CreateDiscountProductCommandHandler فراخوانی شود
|
||||
/// </summary>
|
||||
/// <param name="productId">شناسه محصول (Product.Id یا DiscountProduct.Id)</param>
|
||||
/// <param name="productType">نوع محصول (RegularProduct یا DiscountProduct)</param>
|
||||
/// <param name="initialQuantity">موجودی اولیه</param>
|
||||
/// <param name="warehouseId">شناسه انبار (پیشفرض: انبار اصلی)</param>
|
||||
/// <param name="lowStockThreshold">آستانه هشدار کمموجودی</param>
|
||||
/// <param name="ct">CancellationToken</param>
|
||||
/// <returns>شناسه InventoryItem ایجاد شده</returns>
|
||||
Task<long> InitializeInventoryAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int initialQuantity,
|
||||
long? warehouseId = null,
|
||||
int lowStockThreshold = 10,
|
||||
CancellationToken ct = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Query Operations
|
||||
|
||||
/// <summary>
|
||||
/// دریافت موجودی یک محصول
|
||||
/// </summary>
|
||||
Task<InventoryItem?> GetInventoryAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
long? warehouseId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت موجودی قابل فروش (Quantity - ReservedQuantity)
|
||||
/// </summary>
|
||||
Task<int> GetAvailableQuantityAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
long? warehouseId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// بررسی اینکه آیا موجودی کافی برای فروش وجود دارد
|
||||
/// </summary>
|
||||
Task<bool> CheckAvailabilityAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int requiredQuantity,
|
||||
long? warehouseId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت لیست محصولات کمموجود
|
||||
/// </summary>
|
||||
Task<List<InventoryItem>> GetLowStockItemsAsync(
|
||||
ProductType? productType = null,
|
||||
long? warehouseId = null,
|
||||
int count = 50,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// دریافت تاریخچه حرکات موجودی
|
||||
/// </summary>
|
||||
Task<List<StockMovement>> GetStockMovementsAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
DateTime? fromDate = null,
|
||||
DateTime? toDate = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Order Flow Operations
|
||||
|
||||
/// <summary>
|
||||
/// رزرو موجودی برای سفارش pending
|
||||
/// این متد در PlaceOrderCommandHandler فراخوانی میشود
|
||||
/// فقط ReservedQuantity را افزایش میدهد، Quantity تغییر نمیکند
|
||||
/// </summary>
|
||||
/// <param name="productId">شناسه محصول</param>
|
||||
/// <param name="productType">نوع محصول</param>
|
||||
/// <param name="quantity">تعداد رزرو</param>
|
||||
/// <param name="orderId">شناسه سفارش (Order.Id یا DiscountOrder.Id)</param>
|
||||
/// <param name="ct">CancellationToken</param>
|
||||
/// <returns>true اگر رزرو موفق بود</returns>
|
||||
Task<bool> ReserveStockAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// آزادسازی رزرو (لغو سفارش یا timeout)
|
||||
/// این متد در CancelOrderCommandHandler فراخوانی میشود
|
||||
/// </summary>
|
||||
Task<bool> ReleaseReservationAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// تایید فروش - کسر واقعی موجودی
|
||||
/// این متد در CompleteOrderPaymentCommandHandler فراخوانی میشود
|
||||
/// ReservedQuantity کاهش مییابد، Quantity کاهش مییابد، Product.RemainingCount sync میشود
|
||||
/// </summary>
|
||||
Task<bool> ConfirmSaleAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Stock Management Operations
|
||||
|
||||
/// <summary>
|
||||
/// ورود کالا به انبار (Restock)
|
||||
/// </summary>
|
||||
/// <param name="productId">شناسه محصول</param>
|
||||
/// <param name="productType">نوع محصول</param>
|
||||
/// <param name="quantity">تعداد ورودی</param>
|
||||
/// <param name="referenceNumber">شماره مرجع (مثل شماره فاکتور خرید)</param>
|
||||
/// <param name="note">یادداشت</param>
|
||||
/// <param name="performedByUserId">شناسه کاربر انجامدهنده</param>
|
||||
/// <param name="ct">CancellationToken</param>
|
||||
Task<bool> AddStockAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
string? referenceNumber = null,
|
||||
string? note = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// تعدیل موجودی (تنظیم به مقدار جدید)
|
||||
/// </summary>
|
||||
Task<bool> AdjustStockAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int newQuantity,
|
||||
string? note = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// ثبت برگشت کالا از مشتری
|
||||
/// </summary>
|
||||
Task<bool> ProcessReturnAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
long? orderId = null,
|
||||
string? note = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// ثبت ضایعات/مفقودی
|
||||
/// </summary>
|
||||
Task<bool> RecordLossAsync(
|
||||
long productId,
|
||||
ProductType productType,
|
||||
int quantity,
|
||||
StockMovementType lossType, // Damaged or Lost
|
||||
string? note = null,
|
||||
long? performedByUserId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk Operations
|
||||
|
||||
/// <summary>
|
||||
/// رزرو موجودی برای چند آیتم (یک سفارش با چند محصول)
|
||||
/// </summary>
|
||||
Task<bool> BulkReserveStockAsync(
|
||||
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// آزادسازی رزرو برای چند آیتم
|
||||
/// </summary>
|
||||
Task<bool> BulkReleaseReservationAsync(
|
||||
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// تایید فروش برای چند آیتم
|
||||
/// </summary>
|
||||
Task<bool> BulkConfirmSaleAsync(
|
||||
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
|
||||
long? orderId = null,
|
||||
CancellationToken ct = default);
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -27,6 +27,24 @@ public interface IPaymentGatewayService
|
||||
string verificationToken,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// تأیید پرداخت با مبلغ — برای درگاههایی مثل زرینپال که مبلغ را در Verify نیاز دارند
|
||||
/// </summary>
|
||||
/// <param name="refId">شماره مرجع تراکنش (Authority در زرینپال)</param>
|
||||
/// <param name="verificationToken">توکن تأیید از درگاه (Status در زرینپال)</param>
|
||||
/// <param name="amountInToman">مبلغ تراکنش به تومان</param>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns>وضعیت نهایی تراکنش</returns>
|
||||
Task<PaymentVerificationResult> VerifyPaymentAsync(
|
||||
string refId,
|
||||
string verificationToken,
|
||||
decimal amountInToman,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// پیشفرض: درگاههایی که Amount نمیخواهند، از overload بدون amount استفاده کنند
|
||||
return VerifyPaymentAsync(refId, verificationToken, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// واریز مبلغ به حساب کاربر (برداشت از کیف پول)
|
||||
/// </summary>
|
||||
@@ -124,6 +142,21 @@ public class PaymentVerificationResult
|
||||
/// پیام
|
||||
/// </summary>
|
||||
public string? Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شماره کارت ماسکشده (مثلاً 6037-****-****-1234)
|
||||
/// </summary>
|
||||
public string? CardPan { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// هش کارت بانکی
|
||||
/// </summary>
|
||||
public string? CardHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// کد وضعیت verify از درگاه (100=موفق، 101=تکراری)
|
||||
/// </summary>
|
||||
public int? VerificationCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Mappings;
|
||||
|
||||
public class UserCartsProfile : IRegister
|
||||
{
|
||||
void IRegister.Register(TypeAdapterConfig config)
|
||||
{
|
||||
config.NewConfig<UserCart,GetAllUserCartsByFilterResponseModel>()
|
||||
.Map(dest => dest.Id, src => src.Id)
|
||||
.Map(dest => dest.Count, src => src.Count)
|
||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
||||
.Map(dest => dest.ProductShortInfomation, src => src.Product.ShortInfomation)
|
||||
.Map(dest => dest.ProductDiscount, src => src.Product.Discount)
|
||||
.Map(dest => dest.ProductPrice, src => src.Product.Price)
|
||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
||||
.Map(dest => dest.Created, src => src.Created)
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
|
||||
using CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
|
||||
|
||||
namespace CMSMicroservice.Application.Common.Mappings;
|
||||
|
||||
public class UserOrderProfile : IRegister
|
||||
{
|
||||
void IRegister.Register(TypeAdapterConfig config)
|
||||
{
|
||||
config.NewConfig<UserOrder,GetUserOrderResponseDto>()
|
||||
.Map(dest => dest.Id, src => src.Id)
|
||||
.Map(dest => dest.Amount, src => src.Amount)
|
||||
.Map(dest => dest.PackageId, src => src.PackageId)
|
||||
.Map(dest => dest.TransactionId, src => src.TransactionId)
|
||||
.Map(dest => dest.PaymentStatus, src => src.PaymentStatus)
|
||||
.Map(dest => dest.PaymentDate, src => src.PaymentDate)
|
||||
.Map(dest => dest.UserId, src => src.UserId)
|
||||
.Map(dest => dest.UserAddressId, src => src.UserAddressId)
|
||||
.Map(dest => dest.PaymentMethod, src => src.PaymentMethod)
|
||||
.Map(dest => dest.UserAddressText, src => src.UserAddress.Address)
|
||||
.Map(dest => dest.FactorDetails, src => src.FactorDetails.Select(s=>s.Adapt<GetUserOrderResponseFactorDetail>()))
|
||||
|
||||
;
|
||||
|
||||
config.NewConfig<UserOrder,GetAllUserOrderByFilterResponseModel>()
|
||||
.Map(dest => dest.Id, src => src.Id)
|
||||
.Map(dest => dest.Amount, src => src.Amount)
|
||||
.Map(dest => dest.PackageId, src => src.PackageId)
|
||||
.Map(dest => dest.TransactionId, src => src.TransactionId)
|
||||
.Map(dest => dest.PaymentStatus, src => src.PaymentStatus)
|
||||
.Map(dest => dest.PaymentDate, src => src.PaymentDate)
|
||||
.Map(dest => dest.UserId, src => src.UserId)
|
||||
.Map(dest => dest.UserAddressId, src => src.UserAddressId)
|
||||
.Map(dest => dest.PaymentMethod, src => src.PaymentMethod)
|
||||
.Map(dest => dest.UserAddressText, src => src.UserAddress.Address)
|
||||
.Map(dest => dest.FactorDetails, src => src.FactorDetails.Select(s=>s.Adapt<GetUserOrderResponseFactorDetail>()))
|
||||
;
|
||||
|
||||
config.NewConfig<FactorDetails,GetUserOrderResponseFactorDetail>()
|
||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
||||
.Map(dest => dest.UnitPrice, src => src.Product.Price)
|
||||
.Map(dest => dest.Count, src => src.Count)
|
||||
.Map(dest => dest.UnitDiscountPrice, src => src.Product.Price*(src.Product.Discount/100))
|
||||
;
|
||||
|
||||
config.NewConfig<FactorDetails,GetAllUserOrderByFilterResponseModelFactorDetail>()
|
||||
.Map(dest => dest.ProductId, src => src.ProductId)
|
||||
.Map(dest => dest.ProductTitle, src => src.Product.Title)
|
||||
.Map(dest => dest.ProductThumbnailPath, src => src.Product.ThumbnailPath)
|
||||
.Map(dest => dest.UnitPrice, src => src.Product.Price)
|
||||
.Map(dest => dest.Count, src => src.Count)
|
||||
.Map(dest => dest.UnitDiscountPrice, src => src.Product.Price*(src.Product.Discount/100))
|
||||
;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
namespace CMSMicroservice.Application.Common.Services;
|
||||
|
||||
/// <summary>
|
||||
/// سرویس محاسبه مالیات بر ارزش افزوده (VAT)
|
||||
/// </summary>
|
||||
public static class VatCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// نرخ VAT ایران - 9 درصد
|
||||
/// </summary>
|
||||
public const decimal VAT_RATE = 0.09m;
|
||||
|
||||
/// <summary>
|
||||
/// نرخ VAT به صورت درصد (9)
|
||||
/// </summary>
|
||||
public const int VAT_PERCENT = 9;
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه VAT از مبلغ خالص
|
||||
/// </summary>
|
||||
/// <param name="netAmount">مبلغ خالص (بدون مالیات)</param>
|
||||
/// <returns>مبلغ VAT</returns>
|
||||
public static long CalculateVat(long netAmount)
|
||||
{
|
||||
return (long)(netAmount * VAT_RATE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه مبلغ ناخالص (شامل VAT) از مبلغ خالص
|
||||
/// </summary>
|
||||
/// <param name="netAmount">مبلغ خالص</param>
|
||||
/// <returns>مبلغ ناخالص (خالص + VAT)</returns>
|
||||
public static long CalculateGrossAmount(long netAmount)
|
||||
{
|
||||
return netAmount + CalculateVat(netAmount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// استخراج مبلغ خالص از مبلغ ناخالص
|
||||
/// </summary>
|
||||
/// <param name="grossAmount">مبلغ ناخالص (شامل VAT)</param>
|
||||
/// <returns>مبلغ خالص</returns>
|
||||
public static long ExtractNetAmount(long grossAmount)
|
||||
{
|
||||
return (long)(grossAmount / (1 + VAT_RATE));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// استخراج VAT از مبلغ ناخالص
|
||||
/// </summary>
|
||||
/// <param name="grossAmount">مبلغ ناخالص (شامل VAT)</param>
|
||||
/// <returns>مبلغ VAT</returns>
|
||||
public static long ExtractVatFromGross(long grossAmount)
|
||||
{
|
||||
return grossAmount - ExtractNetAmount(grossAmount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// جزئیات محاسبه VAT
|
||||
/// </summary>
|
||||
public record VatBreakdown(
|
||||
long NetAmount,
|
||||
long VatAmount,
|
||||
long GrossAmount,
|
||||
decimal VatRate
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// محاسبه کامل جزئیات VAT
|
||||
/// </summary>
|
||||
/// <param name="netAmount">مبلغ خالص</param>
|
||||
/// <returns>جزئیات کامل VAT</returns>
|
||||
public static VatBreakdown CalculateBreakdown(long netAmount)
|
||||
{
|
||||
var vatAmount = CalculateVat(netAmount);
|
||||
return new VatBreakdown(
|
||||
NetAmount: netAmount,
|
||||
VatAmount: vatAmount,
|
||||
GrossAmount: netAmount + vatAmount,
|
||||
VatRate: VAT_RATE
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -210,7 +210,7 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
|
||||
var mainLog = new UserWalletChangeLog
|
||||
{
|
||||
WalletId = wallet.Id,
|
||||
CurrentBalance = wallet.Balance,
|
||||
CurrentBalance = 0,
|
||||
ChangeValue = SystemConstants.DayaLoanAmount,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
@@ -230,7 +230,7 @@ public class CheckAndProcessDayaLoansCommandHandler : IRequestHandler<CheckAndPr
|
||||
ChangeValue = 0,
|
||||
CurrentNetworkBalance = wallet.NetworkBalance,
|
||||
ChangeNerworkValue = 0,
|
||||
CurrentDiscountBalance = wallet.DiscountBalance,
|
||||
CurrentDiscountBalance = 0,
|
||||
ChangeDiscountValue = discountAmount,
|
||||
IsIncrease = true,
|
||||
RefrenceId = transaction.Id
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddDiscountProductImage;
|
||||
|
||||
public class AddDiscountProductImageCommand : IRequest<long>
|
||||
{
|
||||
public long DiscountProductId { get; set; }
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
public string ThumbnailPath { get; set; } = string.Empty;
|
||||
public string? Title { get; set; }
|
||||
public string? AltText { get; set; }
|
||||
|
||||
// Image file upload
|
||||
public byte[]? ImageFileBytes { get; set; }
|
||||
public string? ImageFileMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddDiscountProductImage;
|
||||
|
||||
public class AddDiscountProductImageCommandHandler : IRequestHandler<AddDiscountProductImageCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManager _fileManager;
|
||||
|
||||
public AddDiscountProductImageCommandHandler(IApplicationDbContext context, IFileManager fileManager)
|
||||
{
|
||||
_context = context;
|
||||
_fileManager = fileManager;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(AddDiscountProductImageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Verify product exists
|
||||
var productExists = await _context.DiscountProducts
|
||||
.AnyAsync(p => p.Id == request.DiscountProductId, cancellationToken);
|
||||
|
||||
if (!productExists)
|
||||
throw new InvalidOperationException($"DiscountProduct with Id {request.DiscountProductId} not found.");
|
||||
|
||||
var imagePath = request.ImagePath;
|
||||
var thumbnailPath = request.ThumbnailPath;
|
||||
|
||||
// آپلود تصویر (اگر فایل ارسال شده باشد)
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
var result = await _fileManager.UploadImageAsync(
|
||||
"Images/DiscountProducts/Gallery",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
imagePath = result.Main.Path;
|
||||
thumbnailPath = result.Thumbnail.Path;
|
||||
}
|
||||
|
||||
// Get the max sort order for this product
|
||||
var maxSortOrder = await _context.DiscountProductImages
|
||||
.Where(i => i.DiscountProductId == request.DiscountProductId)
|
||||
.MaxAsync(i => (int?)i.SortOrder, cancellationToken) ?? 0;
|
||||
|
||||
var image = new DiscountProductImage
|
||||
{
|
||||
DiscountProductId = request.DiscountProductId,
|
||||
ImagePath = imagePath,
|
||||
ThumbnailPath = thumbnailPath,
|
||||
Title = request.Title,
|
||||
AltText = request.AltText,
|
||||
SortOrder = maxSortOrder + 1,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
_context.DiscountProductImages.Add(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return image.Id;
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCustomerCart;
|
||||
|
||||
/// <summary>
|
||||
/// Command برای افزودن محصول به سبد خرید کاربر فعلی
|
||||
/// </summary>
|
||||
public class AddToCustomerCartCommand : IRequest<AddToCustomerCartCommandResponse>
|
||||
{
|
||||
public long ProductId { get; set; }
|
||||
public int Count { get; set; }
|
||||
// UserId from ICurrentUserService
|
||||
}
|
||||
|
||||
public class AddToCustomerCartCommandResponse
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCustomerCart;
|
||||
|
||||
public class AddToCustomerCartCommandHandler : IRequestHandler<AddToCustomerCartCommand, AddToCustomerCartCommandResponse>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public AddToCustomerCartCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<AddToCustomerCartCommandResponse> Handle(AddToCustomerCartCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Extract UserId from JWT token
|
||||
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||
if (userId == 0)
|
||||
{
|
||||
throw new UnauthorizedAccessException("User not authenticated");
|
||||
}
|
||||
|
||||
// Check if product exists and is not deleted
|
||||
var product = await _context.Products
|
||||
.FirstOrDefaultAsync(p => p.Id == request.ProductId && !p.IsDeleted, cancellationToken);
|
||||
|
||||
if (product == null)
|
||||
{
|
||||
return new AddToCustomerCartCommandResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "محصول یافت نشد یا حذف شده است"
|
||||
};
|
||||
}
|
||||
|
||||
// Check if item already exists in cart
|
||||
var existingCartItem = await _context.UserCarts
|
||||
.FirstOrDefaultAsync(uc => uc.UserId == userId && uc.ProductId == request.ProductId, cancellationToken);
|
||||
|
||||
if (existingCartItem != null)
|
||||
{
|
||||
// Update count
|
||||
existingCartItem.Count += request.Count;
|
||||
_context.UserCarts.Update(existingCartItem);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new AddToCustomerCartCommandResponse
|
||||
{
|
||||
Id = existingCartItem.Id,
|
||||
Success = true,
|
||||
Message = "تعداد محصول در سبد خرید بهروزرسانی شد"
|
||||
};
|
||||
}
|
||||
|
||||
// Create new cart item
|
||||
var cartItem = new UserCart
|
||||
{
|
||||
UserId = userId,
|
||||
ProductId = request.ProductId,
|
||||
Count = request.Count,
|
||||
Created = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.UserCarts.Add(cartItem);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new AddToCustomerCartCommandResponse
|
||||
{
|
||||
Id = cartItem.Id,
|
||||
Success = true,
|
||||
Message = "محصول به سبد خرید اضافه شد"
|
||||
};
|
||||
}
|
||||
}
|
||||
+28
-7
@@ -8,10 +8,14 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayme
|
||||
public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderPaymentCommand, CompleteOrderPaymentResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IInventoryService _inventoryService;
|
||||
|
||||
public CompleteOrderPaymentCommandHandler(IApplicationDbContext context)
|
||||
public CompleteOrderPaymentCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IInventoryService inventoryService)
|
||||
{
|
||||
_context = context;
|
||||
_inventoryService = inventoryService;
|
||||
}
|
||||
|
||||
public async Task<CompleteOrderPaymentResponseDto> Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken)
|
||||
@@ -52,7 +56,7 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
||||
// Update order
|
||||
order.PaymentStatus = PaymentStatus.Success;
|
||||
order.PaymentDate = DateTime.Now;
|
||||
order.DeliveryStatus = DeliveryStatus.InTransit;
|
||||
order.DeliveryStatus = DeliveryStatus.Pending;
|
||||
|
||||
// Deduct discount balance from user wallet
|
||||
var userWallet = await _context.UserWallets
|
||||
@@ -63,12 +67,18 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
||||
userWallet.DiscountBalance -= order.DiscountBalanceUsed;
|
||||
}
|
||||
|
||||
// Update product stock and sale count
|
||||
// تایید فروش و کسر موجودی از طریق InventoryService
|
||||
foreach (var orderDetail in order.OrderDetails)
|
||||
{
|
||||
var product = orderDetail.Product;
|
||||
product.RemainingCount -= orderDetail.Count;
|
||||
product.SaleCount += orderDetail.Count;
|
||||
await _inventoryService.ConfirmSaleAsync(
|
||||
orderDetail.ProductId,
|
||||
ProductType.DiscountProduct,
|
||||
orderDetail.Count,
|
||||
order.Id,
|
||||
cancellationToken);
|
||||
|
||||
// افزایش تعداد فروش
|
||||
orderDetail.Product.SaleCount += orderDetail.Count;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
@@ -82,9 +92,20 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
|
||||
}
|
||||
else
|
||||
{
|
||||
// Payment failed
|
||||
// Payment failed - آزادسازی رزرو
|
||||
foreach (var orderDetail in order.OrderDetails)
|
||||
{
|
||||
await _inventoryService.ReleaseReservationAsync(
|
||||
orderDetail.ProductId,
|
||||
ProductType.DiscountProduct,
|
||||
orderDetail.Count,
|
||||
order.Id,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
transaction.PaymentStatus = PaymentStatus.Reject;
|
||||
order.PaymentStatus = PaymentStatus.Reject;
|
||||
order.DeliveryStatus = DeliveryStatus.Cancelled;
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
|
||||
+10
-1
@@ -11,6 +11,15 @@ public class CreateDiscountProductCommand : IRequest<long>
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public string ImagePath { get; set; }
|
||||
public string ThumbnailPath { get; set; }
|
||||
public int RemainingCount { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public List<long> CategoryIds { get; set; } = new();
|
||||
|
||||
// Image file upload
|
||||
public byte[]? ImageFileBytes { get; set; }
|
||||
public string? ImageFileMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
public byte[]? ThumbnailFileBytes { get; set; }
|
||||
public string? ThumbnailFileMime { get; set; }
|
||||
public string? ThumbnailFileName { get; set; }
|
||||
}
|
||||
|
||||
+48
-5
@@ -1,5 +1,7 @@
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -8,10 +10,17 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.CreateDiscountProd
|
||||
public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscountProductCommand, long>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IInventoryService _inventoryService;
|
||||
private readonly IFileManager _fileManager;
|
||||
|
||||
public CreateDiscountProductCommandHandler(IApplicationDbContext context)
|
||||
public CreateDiscountProductCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IInventoryService inventoryService,
|
||||
IFileManager fileManager)
|
||||
{
|
||||
_context = context;
|
||||
_inventoryService = inventoryService;
|
||||
_fileManager = fileManager;
|
||||
}
|
||||
|
||||
public async Task<long> Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken)
|
||||
@@ -23,18 +32,52 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
|
||||
FullInformation = request.FullInformation,
|
||||
Price = request.Price,
|
||||
MaxDiscountPercent = request.MaxDiscountPercent,
|
||||
ImagePath = request.ImagePath,
|
||||
ThumbnailPath = request.ThumbnailPath,
|
||||
RemainingCount = request.RemainingCount,
|
||||
ImagePath = request.ImagePath ?? string.Empty,
|
||||
ThumbnailPath = request.ThumbnailPath ?? string.Empty,
|
||||
RemainingCount = 0,
|
||||
Rate = 0,
|
||||
SaleCount = 0,
|
||||
ViewCount = 0,
|
||||
IsActive = true
|
||||
IsActive = request.IsActive
|
||||
};
|
||||
|
||||
// آپلود تصویر اصلی (اگر فایل ارسال شده باشد)
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
var result = await _fileManager.UploadImageAsync(
|
||||
"Images/DiscountProducts",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
product.ImagePath = result.Main.Path;
|
||||
product.ThumbnailPath = result.Thumbnail.Path;
|
||||
}
|
||||
|
||||
// آپلود بندانگشتی جداگانه (اختیاری)
|
||||
if (request.ThumbnailFileBytes is { Length: > 0 })
|
||||
{
|
||||
var thumbResult = await _fileManager.UploadAsync(
|
||||
"Images/DiscountProducts/Thumbnails",
|
||||
request.ThumbnailFileBytes,
|
||||
request.ThumbnailFileMime ?? "image/jpeg",
|
||||
request.ThumbnailFileName,
|
||||
cancellationToken);
|
||||
|
||||
product.ThumbnailPath = thumbResult.Path;
|
||||
}
|
||||
|
||||
_context.DiscountProducts.Add(product);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// ایجاد رکورد موجودی در سیستم انبارداری با موجودی اولیه صفر
|
||||
await _inventoryService.InitializeInventoryAsync(
|
||||
product.Id,
|
||||
ProductType.DiscountProduct,
|
||||
0, // موجودی اولیه صفر - باید از طریق Inventory اضافه شود
|
||||
ct: cancellationToken);
|
||||
|
||||
// Add product categories
|
||||
if (request.CategoryIds.Any())
|
||||
{
|
||||
|
||||
+4
-13
@@ -11,26 +11,17 @@ public class CreateDiscountProductCommandValidator : AbstractValidator<CreateDis
|
||||
.MaximumLength(200).WithMessage("عنوان محصول نمیتواند بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(v => v.ShortInfomation)
|
||||
.NotEmpty().WithMessage("توضیحات کوتاه الزامی است")
|
||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نمیتواند بیشتر از 500 کاراکتر باشد");
|
||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نمیتواند بیشتر از 500 کاراکتر باشد")
|
||||
.When(v => !string.IsNullOrEmpty(v.ShortInfomation));
|
||||
|
||||
RuleFor(v => v.FullInformation)
|
||||
.NotEmpty().WithMessage("توضیحات کامل الزامی است")
|
||||
.MaximumLength(2000).WithMessage("توضیحات کامل نمیتواند بیشتر از 2000 کاراکتر باشد");
|
||||
.MaximumLength(10000).WithMessage("توضیحات کامل نمیتواند بیشتر از 10000 کاراکتر باشد")
|
||||
.When(v => !string.IsNullOrEmpty(v.FullInformation));
|
||||
|
||||
RuleFor(v => v.Price)
|
||||
.GreaterThan(0).WithMessage("قیمت باید بیشتر از صفر باشد");
|
||||
|
||||
RuleFor(v => v.MaxDiscountPercent)
|
||||
.InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
|
||||
|
||||
RuleFor(v => v.RemainingCount)
|
||||
.GreaterThanOrEqualTo(0).WithMessage("موجودی نمیتواند منفی باشد");
|
||||
|
||||
RuleFor(v => v.ImagePath)
|
||||
.NotEmpty().WithMessage("تصویر محصول الزامی است");
|
||||
|
||||
RuleFor(v => v.ThumbnailPath)
|
||||
.NotEmpty().WithMessage("تصویر بندانگشتی الزامی است");
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.DeleteDiscountProductImage;
|
||||
|
||||
public class DeleteDiscountProductImageCommand : IRequest<bool>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.DeleteDiscountProductImage;
|
||||
|
||||
public class DeleteDiscountProductImageCommandHandler : IRequestHandler<DeleteDiscountProductImageCommand, bool>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public DeleteDiscountProductImageCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(DeleteDiscountProductImageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var image = await _context.DiscountProductImages
|
||||
.FirstOrDefaultAsync(i => i.Id == request.Id, cancellationToken);
|
||||
|
||||
if (image == null)
|
||||
return false;
|
||||
|
||||
_context.DiscountProductImages.Remove(image);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Reorder remaining images for this product
|
||||
var remainingImages = await _context.DiscountProductImages
|
||||
.Where(i => i.DiscountProductId == image.DiscountProductId)
|
||||
.OrderBy(i => i.SortOrder)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
for (int i = 0; i < remainingImages.Count; i++)
|
||||
{
|
||||
remainingImages[i].SortOrder = i + 1;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+5
@@ -18,4 +18,9 @@ public class PlaceOrderResponseDto
|
||||
public long TotalAmount { get; set; }
|
||||
public long DiscountBalanceUsed { get; set; }
|
||||
public long GatewayAmountRequired { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// URL درگاه پرداخت — اگر null باشد یعنی نیاز به پرداخت آنلاین نیست
|
||||
/// </summary>
|
||||
public string? PaymentUrl { get; set; }
|
||||
}
|
||||
|
||||
+145
-17
@@ -1,19 +1,35 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Application.Common.Services;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using CMSMicroservice.Domain.Entities.Payment;
|
||||
using CMSMicroservice.Domain.Enums;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
|
||||
|
||||
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, PlaceOrderResponseDto>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IInventoryService _inventoryService;
|
||||
private readonly IPaymentGatewayService _paymentGateway;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<PlaceOrderCommandHandler> _logger;
|
||||
|
||||
public PlaceOrderCommandHandler(IApplicationDbContext context)
|
||||
public PlaceOrderCommandHandler(
|
||||
IApplicationDbContext context,
|
||||
IInventoryService inventoryService,
|
||||
IPaymentGatewayService paymentGateway,
|
||||
IConfiguration configuration,
|
||||
ILogger<PlaceOrderCommandHandler> logger)
|
||||
{
|
||||
_context = context;
|
||||
_inventoryService = inventoryService;
|
||||
_paymentGateway = paymentGateway;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
|
||||
@@ -93,25 +109,20 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
||||
});
|
||||
}
|
||||
|
||||
// Validate discount balance usage
|
||||
// Always apply maximum possible discount (100%) — user cannot choose less
|
||||
var maxDiscountBalanceUsable = totalDiscountAmount;
|
||||
var actualDiscountBalanceUsed = Math.Min(request.DiscountBalanceToUse, maxDiscountBalanceUsable);
|
||||
actualDiscountBalanceUsed = Math.Min(actualDiscountBalanceUsed, userWallet.DiscountBalance);
|
||||
var actualDiscountBalanceUsed = Math.Min(maxDiscountBalanceUsable, userWallet.DiscountBalance);
|
||||
|
||||
if (actualDiscountBalanceUsed < request.DiscountBalanceToUse)
|
||||
{
|
||||
return new PlaceOrderResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = $"موجودی تخفیف کافی نیست. حداکثر قابل استفاده: {maxDiscountBalanceUsable:N0} تومان، موجودی شما: {userWallet.DiscountBalance:N0} تومان"
|
||||
};
|
||||
}
|
||||
_logger.LogInformation(
|
||||
"Discount auto-applied: max allowed={MaxAllowed}, wallet balance={WalletBalance}, used={Used}",
|
||||
maxDiscountBalanceUsable, userWallet.DiscountBalance, actualDiscountBalanceUsed);
|
||||
|
||||
var gatewayAmountRequired = totalAmount - actualDiscountBalanceUsed;
|
||||
|
||||
// Calculate VAT (9%)
|
||||
var vatAmount = (gatewayAmountRequired * 9) / 100;
|
||||
var finalGatewayAmount = gatewayAmountRequired + vatAmount;
|
||||
// Calculate VAT using centralized calculator
|
||||
var vatBreakdown = VatCalculator.CalculateBreakdown(gatewayAmountRequired);
|
||||
var vatAmount = vatBreakdown.VatAmount;
|
||||
var finalGatewayAmount = vatBreakdown.GrossAmount;
|
||||
|
||||
// Create transaction for gateway payment
|
||||
var transaction = new Transaction
|
||||
@@ -150,20 +161,137 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
|
||||
|
||||
_context.DiscountOrderDetails.AddRange(orderDetails);
|
||||
|
||||
// رزرو موجودی برای سفارش pending
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
await _inventoryService.ReserveStockAsync(
|
||||
cartItem.ProductId,
|
||||
ProductType.DiscountProduct,
|
||||
cartItem.Count,
|
||||
order.Id,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
// Clear cart
|
||||
_context.DiscountShoppingCarts.RemoveRange(cartItems);
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// اگر مبلغ درگاه > ۰ باشد، باید به درگاه پرداخت متصل شویم
|
||||
string? paymentUrl = null;
|
||||
|
||||
if (finalGatewayAmount > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
// آدرس callback — زرینپال بعد از پرداخت کاربر را به اینجا هدایت میکند
|
||||
var cmsBaseUrl = _configuration["CmsBaseUrl"] ?? "https://localhost:32846";
|
||||
var callbackUrl = $"{cmsBaseUrl}/api/payment/discount-order/callback?orderId={order.Id}";
|
||||
|
||||
// درخواست به درگاه
|
||||
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
|
||||
{
|
||||
Amount = finalGatewayAmount,
|
||||
UserId = request.UserId,
|
||||
Description = $"فروشگاه تخفیفی - سفارش #{order.Id}",
|
||||
CallbackUrl = callbackUrl
|
||||
}, cancellationToken);
|
||||
|
||||
if (paymentResult.IsSuccess && !string.IsNullOrEmpty(paymentResult.GatewayUrl))
|
||||
{
|
||||
// ذخیره Authority/RefId در تراکنش برای verify بعدی
|
||||
transaction.RefId = paymentResult.RefId;
|
||||
|
||||
// ثبت PaymentTransaction — جدول جدید با اطلاعات درگاه
|
||||
var paymentTx = new PaymentTransaction
|
||||
{
|
||||
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
|
||||
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
|
||||
Amount = finalGatewayAmount,
|
||||
CallbackUrl = callbackUrl,
|
||||
Description = $"فروشگاه تخفیفی - سفارش #{order.Id}",
|
||||
Mobile = null,
|
||||
UserId = request.UserId,
|
||||
RequestStatusCode = 100,
|
||||
RequestStatusMessage = "Success",
|
||||
Authority = paymentResult.RefId,
|
||||
PaymentStatus = false, // هنوز verify نشده
|
||||
TransactionId = transaction.Id,
|
||||
OrderId = order.Id.ToString()
|
||||
};
|
||||
_context.PaymentTransactions.Add(paymentTx);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
paymentUrl = paymentResult.GatewayUrl;
|
||||
_logger.LogInformation(
|
||||
"Payment gateway initiated for DiscountOrder #{OrderId}: RefId={RefId}, Url={Url}",
|
||||
order.Id, paymentResult.RefId, paymentResult.GatewayUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError(
|
||||
"Payment gateway initiation failed for DiscountOrder #{OrderId}: {Error}",
|
||||
order.Id, paymentResult.ErrorMessage);
|
||||
|
||||
return new PlaceOrderResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = $"خطا در اتصال به درگاه پرداخت: {paymentResult.ErrorMessage}",
|
||||
OrderId = order.Id
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Payment gateway exception for DiscountOrder #{OrderId}", order.Id);
|
||||
return new PlaceOrderResponseDto
|
||||
{
|
||||
Success = false,
|
||||
Message = $"خطا در اتصال به درگاه پرداخت: {ex.Message}",
|
||||
OrderId = order.Id
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// اگر کل مبلغ از کیف تخفیفی پرداخت شد — مستقیماً تکمیل شود
|
||||
transaction.PaymentStatus = PaymentStatus.Success;
|
||||
transaction.PaymentDate = DateTime.Now;
|
||||
order.PaymentStatus = PaymentStatus.Success;
|
||||
order.PaymentDate = DateTime.Now;
|
||||
order.DeliveryStatus = DeliveryStatus.Pending;
|
||||
|
||||
var walletForDeduct = await _context.UserWallets
|
||||
.FirstOrDefaultAsync(w => w.UserId == request.UserId, cancellationToken);
|
||||
if (walletForDeduct != null)
|
||||
walletForDeduct.DiscountBalance -= actualDiscountBalanceUsed;
|
||||
|
||||
foreach (var cartItem in cartItems)
|
||||
{
|
||||
await _inventoryService.ConfirmSaleAsync(
|
||||
cartItem.ProductId, ProductType.DiscountProduct,
|
||||
cartItem.Count, order.Id, cancellationToken);
|
||||
cartItem.Product.SaleCount += cartItem.Count;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
_logger.LogInformation(
|
||||
"DiscountOrder #{OrderId} fully paid via discount balance ({Amount} T)",
|
||||
order.Id, actualDiscountBalanceUsed);
|
||||
}
|
||||
|
||||
return new PlaceOrderResponseDto
|
||||
{
|
||||
Success = true,
|
||||
Message = "سفارش ایجاد شد. لطفا پرداخت را تکمیل کنید",
|
||||
Message = finalGatewayAmount > 0
|
||||
? "سفارش ایجاد شد. در حال انتقال به درگاه پرداخت..."
|
||||
: "سفارش با موفقیت ثبت و پرداخت شد",
|
||||
OrderId = order.Id,
|
||||
TransactionId = transaction.Id,
|
||||
TotalAmount = totalAmount,
|
||||
DiscountBalanceUsed = actualDiscountBalanceUsed,
|
||||
GatewayAmountRequired = finalGatewayAmount
|
||||
GatewayAmountRequired = finalGatewayAmount,
|
||||
PaymentUrl = paymentUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCustomerCart;
|
||||
|
||||
/// <summary>
|
||||
/// Command برای حذف محصول از سبد خرید
|
||||
/// </summary>
|
||||
public class RemoveFromCustomerCartCommand : IRequest<RemoveFromCustomerCartCommandResponse>
|
||||
{
|
||||
public long CartItemId { get; set; }
|
||||
// UserId from ICurrentUserService
|
||||
}
|
||||
|
||||
public class RemoveFromCustomerCartCommandResponse
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCustomerCart;
|
||||
|
||||
public class RemoveFromCustomerCartCommandHandler : IRequestHandler<RemoveFromCustomerCartCommand, RemoveFromCustomerCartCommandResponse>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public RemoveFromCustomerCartCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<RemoveFromCustomerCartCommandResponse> Handle(RemoveFromCustomerCartCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Extract UserId from JWT token
|
||||
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||
if (userId == 0)
|
||||
{
|
||||
throw new UnauthorizedAccessException("User not authenticated");
|
||||
}
|
||||
|
||||
// Find and remove cart item
|
||||
var cartItem = await _context.UserCarts
|
||||
.FirstOrDefaultAsync(uc => uc.Id == request.CartItemId && uc.UserId == userId, cancellationToken);
|
||||
|
||||
if (cartItem == null)
|
||||
{
|
||||
return new RemoveFromCustomerCartCommandResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "آیتم سبد خرید یافت نشد"
|
||||
};
|
||||
}
|
||||
|
||||
_context.UserCarts.Remove(cartItem);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new RemoveFromCustomerCartCommandResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "آیتم از سبد خرید حذف شد"
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.ReorderDiscountProductImages;
|
||||
|
||||
public class ReorderDiscountProductImagesCommand : IRequest<bool>
|
||||
{
|
||||
public long DiscountProductId { get; set; }
|
||||
public List<long> ImageIds { get; set; } = new();
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.ReorderDiscountProductImages;
|
||||
|
||||
public class ReorderDiscountProductImagesCommandHandler : IRequestHandler<ReorderDiscountProductImagesCommand, bool>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
|
||||
public ReorderDiscountProductImagesCommandHandler(IApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task<bool> Handle(ReorderDiscountProductImagesCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var images = await _context.DiscountProductImages
|
||||
.Where(i => i.DiscountProductId == request.DiscountProductId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (!images.Any())
|
||||
return false;
|
||||
|
||||
// Validate all image IDs belong to this product
|
||||
var imageIdSet = images.Select(i => i.Id).ToHashSet();
|
||||
if (!request.ImageIds.All(id => imageIdSet.Contains(id)))
|
||||
return false;
|
||||
|
||||
// Update sort order based on the new order
|
||||
for (int i = 0; i < request.ImageIds.Count; i++)
|
||||
{
|
||||
var image = images.FirstOrDefault(img => img.Id == request.ImageIds[i]);
|
||||
if (image != null)
|
||||
{
|
||||
image.SortOrder = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCustomerCartItem;
|
||||
|
||||
/// <summary>
|
||||
/// Command برای بهروزرسانی تعداد محصول در سبد خرید
|
||||
/// </summary>
|
||||
public class UpdateCustomerCartItemCommand : IRequest<UpdateCustomerCartItemCommandResponse>
|
||||
{
|
||||
public long CartItemId { get; set; }
|
||||
public int Count { get; set; }
|
||||
// UserId from ICurrentUserService
|
||||
}
|
||||
|
||||
public class UpdateCustomerCartItemCommandResponse
|
||||
{
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCustomerCartItem;
|
||||
|
||||
public class UpdateCustomerCartItemCommandHandler : IRequestHandler<UpdateCustomerCartItemCommand, UpdateCustomerCartItemCommandResponse>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly ICurrentUserService _currentUser;
|
||||
|
||||
public UpdateCustomerCartItemCommandHandler(IApplicationDbContext context, ICurrentUserService currentUser)
|
||||
{
|
||||
_context = context;
|
||||
_currentUser = currentUser;
|
||||
}
|
||||
|
||||
public async Task<UpdateCustomerCartItemCommandResponse> Handle(UpdateCustomerCartItemCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
// Extract UserId from JWT token
|
||||
var userId = long.TryParse(_currentUser.UserId, out var id) ? id : 0;
|
||||
if (userId == 0)
|
||||
{
|
||||
throw new UnauthorizedAccessException("User not authenticated");
|
||||
}
|
||||
|
||||
// Find cart item
|
||||
var cartItem = await _context.UserCarts
|
||||
.FirstOrDefaultAsync(uc => uc.Id == request.CartItemId && uc.UserId == userId, cancellationToken);
|
||||
|
||||
if (cartItem == null)
|
||||
{
|
||||
return new UpdateCustomerCartItemCommandResponse
|
||||
{
|
||||
Success = false,
|
||||
Message = "آیتم سبد خرید یافت نشد"
|
||||
};
|
||||
}
|
||||
|
||||
// Update count
|
||||
if (request.Count <= 0)
|
||||
{
|
||||
// Remove item if count is 0 or negative
|
||||
_context.UserCarts.Remove(cartItem);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new UpdateCustomerCartItemCommandResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "آیتم از سبد خرید حذف شد"
|
||||
};
|
||||
}
|
||||
|
||||
cartItem.Count = request.Count;
|
||||
_context.UserCarts.Update(cartItem);
|
||||
await _context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new UpdateCustomerCartItemCommandResponse
|
||||
{
|
||||
Success = true,
|
||||
Message = "تعداد آیتم بهروزرسانی شد"
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
@@ -12,7 +12,16 @@ public class UpdateDiscountProductCommand : IRequest<Unit>
|
||||
public int MaxDiscountPercent { get; set; }
|
||||
public string ImagePath { get; set; }
|
||||
public string ThumbnailPath { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public int RemainingCount { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public List<long> CategoryIds { get; set; } = new();
|
||||
|
||||
// Image file upload
|
||||
public byte[]? ImageFileBytes { get; set; }
|
||||
public string? ImageFileMime { get; set; }
|
||||
public string? ImageFileName { get; set; }
|
||||
public byte[]? ThumbnailFileBytes { get; set; }
|
||||
public string? ThumbnailFileMime { get; set; }
|
||||
public string? ThumbnailFileName { get; set; }
|
||||
}
|
||||
|
||||
+39
-3
@@ -1,3 +1,4 @@
|
||||
using CMSMicroservice.Application.Common.FileManager;
|
||||
using CMSMicroservice.Application.Common.Interfaces;
|
||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||
using MediatR;
|
||||
@@ -8,10 +9,12 @@ namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateDiscountProd
|
||||
public class UpdateDiscountProductCommandHandler : IRequestHandler<UpdateDiscountProductCommand, Unit>
|
||||
{
|
||||
private readonly IApplicationDbContext _context;
|
||||
private readonly IFileManager _fileManager;
|
||||
|
||||
public UpdateDiscountProductCommandHandler(IApplicationDbContext context)
|
||||
public UpdateDiscountProductCommandHandler(IApplicationDbContext context, IFileManager fileManager)
|
||||
{
|
||||
_context = context;
|
||||
_fileManager = fileManager;
|
||||
}
|
||||
|
||||
public async Task<Unit> Handle(UpdateDiscountProductCommand request, CancellationToken cancellationToken)
|
||||
@@ -27,11 +30,44 @@ public class UpdateDiscountProductCommandHandler : IRequestHandler<UpdateDiscoun
|
||||
product.FullInformation = request.FullInformation;
|
||||
product.Price = request.Price;
|
||||
product.MaxDiscountPercent = request.MaxDiscountPercent;
|
||||
product.ImagePath = request.ImagePath;
|
||||
product.ThumbnailPath = request.ThumbnailPath;
|
||||
product.RemainingCount = request.RemainingCount;
|
||||
product.IsActive = request.IsActive;
|
||||
|
||||
// آپلود تصویر اصلی (اگر فایل جدید ارسال شده باشد)
|
||||
if (request.ImageFileBytes is { Length: > 0 })
|
||||
{
|
||||
var result = await _fileManager.UploadImageAsync(
|
||||
"Images/DiscountProducts",
|
||||
request.ImageFileBytes,
|
||||
request.ImageFileMime ?? "image/jpeg",
|
||||
request.ImageFileName,
|
||||
cancellationToken);
|
||||
|
||||
product.ImagePath = result.Main.Path;
|
||||
product.ThumbnailPath = result.Thumbnail.Path;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(request.ImagePath))
|
||||
{
|
||||
product.ImagePath = request.ImagePath;
|
||||
}
|
||||
|
||||
// آپلود بندانگشتی جداگانه (اختیاری)
|
||||
if (request.ThumbnailFileBytes is { Length: > 0 })
|
||||
{
|
||||
var thumbResult = await _fileManager.UploadAsync(
|
||||
"Images/DiscountProducts/Thumbnails",
|
||||
request.ThumbnailFileBytes,
|
||||
request.ThumbnailFileMime ?? "image/jpeg",
|
||||
request.ThumbnailFileName,
|
||||
cancellationToken);
|
||||
|
||||
product.ThumbnailPath = thumbResult.Path;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(request.ThumbnailPath))
|
||||
{
|
||||
product.ThumbnailPath = request.ThumbnailPath;
|
||||
}
|
||||
|
||||
// Update categories
|
||||
var existingCategories = await _context.DiscountProductCategories
|
||||
.Where(pc => pc.ProductId == request.ProductId)
|
||||
|
||||
+4
-12
@@ -14,12 +14,12 @@ public class UpdateDiscountProductCommandValidator : AbstractValidator<UpdateDis
|
||||
.MaximumLength(200).WithMessage("عنوان محصول نباید بیشتر از 200 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.ShortInfomation)
|
||||
.NotEmpty().WithMessage("توضیحات کوتاه الزامی است")
|
||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نباید بیشتر از 500 کاراکتر باشد");
|
||||
.MaximumLength(500).WithMessage("توضیحات کوتاه نباید بیشتر از 500 کاراکتر باشد")
|
||||
.When(x => !string.IsNullOrEmpty(x.ShortInfomation));
|
||||
|
||||
RuleFor(x => x.FullInformation)
|
||||
.NotEmpty().WithMessage("توضیحات کامل الزامی است")
|
||||
.MaximumLength(5000).WithMessage("توضیحات کامل نباید بیشتر از 5000 کاراکتر باشد");
|
||||
.MaximumLength(10000).WithMessage("توضیحات کامل نباید بیشتر از 10000 کاراکتر باشد")
|
||||
.When(x => !string.IsNullOrEmpty(x.FullInformation));
|
||||
|
||||
RuleFor(x => x.Price)
|
||||
.GreaterThan(0).WithMessage("قیمت باید بزرگتر از صفر باشد");
|
||||
@@ -27,14 +27,6 @@ public class UpdateDiscountProductCommandValidator : AbstractValidator<UpdateDis
|
||||
RuleFor(x => x.MaxDiscountPercent)
|
||||
.InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
|
||||
|
||||
RuleFor(x => x.ImagePath)
|
||||
.NotEmpty().WithMessage("مسیر تصویر اصلی الزامی است")
|
||||
.MaximumLength(500).WithMessage("مسیر تصویر نباید بیشتر از 500 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.ThumbnailPath)
|
||||
.NotEmpty().WithMessage("مسیر تصویر بندانگشتی الزامی است")
|
||||
.MaximumLength(500).WithMessage("مسیر تصویر نباید بیشتر از 500 کاراکتر باشد");
|
||||
|
||||
RuleFor(x => x.RemainingCount)
|
||||
.GreaterThanOrEqualTo(0).WithMessage("موجودی نمیتواند منفی باشد");
|
||||
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
using MediatR;
|
||||
|
||||
namespace CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateDiscountProductImage;
|
||||
|
||||
public class UpdateDiscountProductImageCommand : IRequest<bool>
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public string ImagePath { get; set; } = string.Empty;
|
||||
public string ThumbnailPath { get; set; } = string.Empty;
|
||||
public string? Title { get; set; }
|
||||
public string? AltText { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user