feat: enhance user club feature management with sorting and creation timestamps

This commit is contained in:
masoodafar-web
2025-12-19 00:30:47 +03:30
parent c7211fb9a7
commit 3cb95dc349
13 changed files with 3627 additions and 20 deletions
@@ -31,8 +31,11 @@ public class GetUserClubFeaturesQueryHandler : IRequestHandler<GetUserClubFeatur
FeatureDescription = ucf.ClubFeature.Description, FeatureDescription = ucf.ClubFeature.Description,
IsActive = ucf.IsActive, IsActive = ucf.IsActive,
GrantedAt = ucf.GrantedAt, GrantedAt = ucf.GrantedAt,
Notes = ucf.Notes Notes = ucf.Notes,
SortOrder = ucf.ClubFeature.SortOrder,
CreatedAt = ucf.Created
}) })
.OrderBy(ucf => ucf.SortOrder)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
return userClubFeatures; return userClubFeatures;
@@ -31,7 +31,7 @@ public class UserClubFeatureDto
public string FeatureTitle { get; set; } public string FeatureTitle { get; set; }
/// <summary> /// <summary>
/// توضیحات ویژگی /// توضیحات کوتاه ویژگی
/// </summary> /// </summary>
public string? FeatureDescription { get; set; } public string? FeatureDescription { get; set; }
@@ -46,7 +46,17 @@ public class UserClubFeatureDto
public DateTime GrantedAt { get; set; } public DateTime GrantedAt { get; set; }
/// <summary> /// <summary>
/// یادداشت /// یادداشت (برای نمایش در مدال جزئیات)
/// </summary> /// </summary>
public string? Notes { get; set; } public string? Notes { get; set; }
/// <summary>
/// ترتیب نمایش
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// تاریخ ایجاد رکورد
/// </summary>
public DateTime CreatedAt { get; set; }
} }
@@ -4,7 +4,7 @@ public record UpdateUserAddressCommand : IRequest<Unit>
//شناسه //شناسه
public long Id { get; init; } public long Id { get; init; }
//شناسه کاربر //شناسه کاربر
public long UserId { get; init; }
//عنوان //عنوان
public string Title { get; init; } public string Title { get; init; }
//آدرس //آدرس
@@ -5,8 +5,7 @@ public class UpdateUserAddressCommandValidator : AbstractValidator<UpdateUserAdd
{ {
RuleFor(model => model.Id) RuleFor(model => model.Id)
.NotNull(); .NotNull();
RuleFor(model => model.UserId)
.NotNull();
RuleFor(model => model.Title) RuleFor(model => model.Title)
.NotEmpty(); .NotEmpty();
RuleFor(model => model.Address) RuleFor(model => model.Address)
@@ -32,6 +32,34 @@ public class
if (user.UserCarts.Count == 0) if (user.UserCarts.Count == 0)
throw new NotFoundException("UserCart", request.UserId); throw new NotFoundException("UserCart", request.UserId);
// چک موجودی محصولات
var outOfStockProducts = new List<string>();
var insufficientStockProducts = new List<(string Title, int Requested, int Available)>();
foreach (var cartItem in user.UserCarts)
{
if (cartItem.Product.RemainingCount <= 0)
{
outOfStockProducts.Add(cartItem.Product.Title);
}
else if (cartItem.Product.RemainingCount < cartItem.Count)
{
insufficientStockProducts.Add((cartItem.Product.Title, cartItem.Count, cartItem.Product.RemainingCount));
}
}
if (outOfStockProducts.Any())
{
throw new Exception($"محصولات زیر ناموجود شده‌اند: {string.Join("، ", outOfStockProducts)}");
}
if (insufficientStockProducts.Any())
{
var messages = insufficientStockProducts.Select(p =>
$"«{p.Title}»: درخواست {p.Requested} عدد، موجودی {p.Available} عدد");
throw new Exception($"موجودی محصولات زیر کافی نیست: {string.Join(" | ", messages)}");
}
if (user.UserCarts.Sum(s => s.Count * s.Product.Price) != request.TotalAmount) if (user.UserCarts.Sum(s => s.Count * s.Product.Price) != request.TotalAmount)
throw new Exception("مبلغ سفارش با مجموع سبد خرید مطابقت ندارد."); throw new Exception("مبلغ سفارش با مجموع سبد خرید مطابقت ندارد.");
@@ -100,8 +128,20 @@ public class
OrderId = newOrder.Id OrderId = newOrder.Id
}); });
await _context.FactorDetails.AddRangeAsync(factorDetailsList, cancellationToken); await _context.FactorDetails.AddRangeAsync(factorDetailsList, cancellationToken);
// کاهش موجودی محصولات و افزایش تعداد فروش
foreach (var cartItem in user.UserCarts)
{
cartItem.Product.RemainingCount -= cartItem.Count;
cartItem.Product.SaleCount += cartItem.Count;
}
user.UserCarts.Clear(); user.UserCarts.Clear();
await _context.SaveChangesAsync(cancellationToken); await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Order {OrderId} completed. Stock updated for {ProductCount} products.",
newOrder.Id, factorDetailsList.Count());
var finalResult = new SubmitShopBuyOrderResponseDto() var finalResult = new SubmitShopBuyOrderResponseDto()
{ {
Id = newOrder.Id, Id = newOrder.Id,
@@ -11,19 +11,17 @@ public class ClubFeature : BaseAuditableEntity
public string Title { get; set; } public string Title { get; set; }
/// <summary> /// <summary>
/// توضیحات /// توضیحات کوتاه
/// </summary> /// </summary>
public string? Description { get; set; } public string? Description { get; set; }
/// <summary> /// <summary>
/// وضعیت فعال/غیرفعال /// وضعیت فعال/غیرفعال
/// </summary> /// </summary>
public bool IsActive { get; set; } public bool IsActive { get; set; }
/// <summary>
/// امتیاز لازم برای دریافت (اختیاری)
/// </summary>
public int? RequiredPoints { get; set; }
/// <summary> /// <summary>
/// ترتیب نمایش /// ترتیب نمایش
@@ -19,7 +19,6 @@ public class ClubFeatureConfiguration : IEntityTypeConfiguration<ClubFeature>
builder.Property(entity => entity.Title).IsRequired().HasMaxLength(200); builder.Property(entity => entity.Title).IsRequired().HasMaxLength(200);
builder.Property(entity => entity.Description).IsRequired(false).HasMaxLength(1000); builder.Property(entity => entity.Description).IsRequired(false).HasMaxLength(1000);
builder.Property(entity => entity.IsActive).IsRequired(); builder.Property(entity => entity.IsActive).IsRequired();
builder.Property(entity => entity.RequiredPoints).IsRequired(false);
builder.Property(entity => entity.SortOrder).IsRequired(); builder.Property(entity => entity.SortOrder).IsRequired();
// Index برای IsActive و SortOrder // Index برای IsActive و SortOrder
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class u15 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RequiredPoints",
schema: "CMS",
table: "ClubFeatures");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "RequiredPoints",
schema: "CMS",
table: "ClubFeatures",
type: "int",
nullable: true);
}
}
}
@@ -106,9 +106,6 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
b.Property<string>("LastModifiedBy") b.Property<string>("LastModifiedBy")
.HasColumnType("nvarchar(max)"); .HasColumnType("nvarchar(max)");
b.Property<int?>("RequiredPoints")
.HasColumnType("int");
b.Property<int>("SortOrder") b.Property<int>("SortOrder")
.HasColumnType("int"); .HasColumnType("int");
@@ -249,6 +249,8 @@ message UserClubFeatureModel
bool is_active = 7; bool is_active = 7;
google.protobuf.Timestamp granted_at = 8; google.protobuf.Timestamp granted_at = 8;
string notes = 9; string notes = 9;
int32 sort_order = 10;
google.protobuf.Timestamp created_at = 11;
} }
// ToggleUserClubFeature Command // ToggleUserClubFeature Command
@@ -60,7 +60,8 @@ message DeactivateConfigurationRequest
// GetByKey Query // GetByKey Query
message GetConfigurationByKeyRequest message GetConfigurationByKeyRequest
{ {
string key = 1; int32 scope = 1;
string key = 2;
} }
message GetConfigurationByKeyResponse message GetConfigurationByKeyResponse
@@ -77,11 +78,17 @@ message GetConfigurationByKeyResponse
// GetAll Query // GetAll Query
message GetAllConfigurationsRequest message GetAllConfigurationsRequest
{
messages.PaginationState pagination_state = 1;
google.protobuf.StringValue sort_by = 2;
GetAllConfigurationsFilter filter = 3;
}
message GetAllConfigurationsFilter
{ {
google.protobuf.Int32Value scope = 1; google.protobuf.Int32Value scope = 1;
google.protobuf.BoolValue is_active = 2; google.protobuf.StringValue key_contains = 2;
int32 page_index = 3; google.protobuf.BoolValue is_active = 3;
int32 page_size = 4;
} }
message GetAllConfigurationsResponse message GetAllConfigurationsResponse
@@ -26,7 +26,9 @@ public class ClubFeatureProfile : IRegister
.Map(dest => dest.FeatureDescription, src => src.FeatureDescription ?? "") .Map(dest => dest.FeatureDescription, src => src.FeatureDescription ?? "")
.Map(dest => dest.IsActive, src => src.IsActive) .Map(dest => dest.IsActive, src => src.IsActive)
.Map(dest => dest.GrantedAt, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.GrantedAt, DateTimeKind.Utc))) .Map(dest => dest.GrantedAt, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.GrantedAt, DateTimeKind.Utc)))
.Map(dest => dest.Notes, src => src.Notes ?? ""); .Map(dest => dest.Notes, src => src.Notes ?? "")
.Map(dest => dest.SortOrder, src => src.SortOrder)
.Map(dest => dest.CreatedAt, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.CreatedAt, DateTimeKind.Utc)));
// List<UserClubFeatureDto> → GetUserClubFeaturesResponse // List<UserClubFeatureDto> → GetUserClubFeaturesResponse
config.NewConfig<List<UserClubFeatureDto>, GetUserClubFeaturesResponse>() config.NewConfig<List<UserClubFeatureDto>, GetUserClubFeaturesResponse>()