Merge branch 'kub-stage' into production
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:
masoodafar-web
2026-02-17 00:53:28 +03:30
587 changed files with 54852 additions and 5122 deletions
@@ -5,6 +5,8 @@
<Nullable>enable</Nullable>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..\..</DockerfileContext>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -12,7 +14,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.23.3" />
<PackageReference Include="Google.Protobuf" Version="3.27.0" />
<PackageReference Include="Grpc.AspNetCore" Version="2.54.0" />
<PackageReference Include="Grpc.AspNetCore.Web" Version="2.54.0" />
<PackageReference Include="Grpc.Net.Client" Version="2.54.0" />
@@ -29,6 +31,7 @@
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
<PackageReference Include="Microsoft.AspNetCore.Grpc.Swagger" Version="0.3.8" />
<PackageReference Include="Microsoft.AspNetCore.Grpc.JsonTranscoding" Version="9.0.11" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
@@ -1,13 +1,20 @@
using Google.Protobuf;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.Common.Interfaces;
using System.Text.RegularExpressions;
namespace CMSMicroservice.WebApi.Common.Behaviours;
public class LoggingBehaviour : Interceptor
public partial class LoggingBehaviour : Interceptor
{
private readonly ILogger _logger;
private readonly ICurrentUserService _currentUserService;
// فیلدهایی که نباید لاگ شوند (بایت‌های تصویر / فایل)
[GeneratedRegex(@"""(File|ImageFile|image_file|file)"":\s*\{[^}]*\}", RegexOptions.Singleline)]
private static partial Regex BinaryFieldPattern();
public LoggingBehaviour(ILogger<LoggingBehaviour> logger, ICurrentUserService currentUserService)
{
_logger = logger;
@@ -21,8 +28,11 @@ public class LoggingBehaviour : Interceptor
{
var requestName = typeof(TRequest).Name;
var userId = _currentUserService.UserId ?? string.Empty;
_logger.LogInformation("gRPC Starting receiving call. Type/Method: {Type} / {Method} Request: {Name} {@UserId} {@Request}",
MethodType.Unary, context.Method , requestName, userId, request);
// لاگ بدون بایت‌های فایل
var safeLog = SanitizeForLog(request);
_logger.LogInformation("gRPC Starting receiving call. Type/Method: {Type} / {Method} Request: {Name} {UserId} {Request}",
MethodType.Unary, context.Method, requestName, userId, safeLog);
try
{
@@ -30,9 +40,26 @@ public class LoggingBehaviour : Interceptor
}
catch (Exception ex)
{
_logger.LogError(ex, "gRPC Request: Unhandled Exception for Request {Name} {@Request}", requestName, request);
_logger.LogError(ex, "gRPC Request: Unhandled Exception for Request {Name} {Request}", requestName, safeLog);
throw;
}
}
/// <summary>
/// حذف بایت‌های فایل از لاگ — جایگزینی با [BINARY DATA]
/// </summary>
private static string SanitizeForLog<T>(T request)
{
if (request is IMessage protoMessage)
{
var json = JsonFormatter.Default.Format(protoMessage);
// حذف محتوای فیلدهای باینری
json = BinaryFieldPattern().Replace(json, "\"$1\": \"[BINARY DATA]\"");
// اگر هنوز رشته‌های base64 طولانی هست، خلاصه کن
if (json.Length > 2000)
return json[..2000] + "... [TRUNCATED]";
return json;
}
return request?.ToString() ?? "";
}
}
@@ -1,15 +1,21 @@
using Google.Protobuf;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Text.RegularExpressions;
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.WebApi.Common.Behaviours;
public class PerformanceBehaviour : Interceptor
public partial class PerformanceBehaviour : Interceptor
{
private readonly Stopwatch _timer;
private readonly ILogger _logger;
private readonly ICurrentUserService _currentUserService;
[GeneratedRegex(@"""(File|ImageFile|image_file|file)"":\s*\{[^}]*\}", RegexOptions.Singleline)]
private static partial Regex BinaryFieldPattern();
public PerformanceBehaviour(ILogger<PerformanceBehaviour> logger, ICurrentUserService currentUserService)
{
_timer = new Stopwatch();
@@ -34,11 +40,25 @@ public class PerformanceBehaviour : Interceptor
{
var requestName = typeof(TRequest).Name;
var userId = _currentUserService.UserId ?? string.Empty;
var safeLog = SanitizeForLog(request);
_logger.LogWarning("gRPC Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserId} {@Request}",
requestName, elapsedMilliseconds, userId, request);
_logger.LogWarning("gRPC Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {UserId} {Request}",
requestName, elapsedMilliseconds, userId, safeLog);
}
return response;
}
private static string SanitizeForLog<T>(T request)
{
if (request is IMessage protoMessage)
{
var json = JsonFormatter.Default.Format(protoMessage);
json = BinaryFieldPattern().Replace(json, "\"$1\": \"[BINARY DATA]\"");
if (json.Length > 2000)
return json[..2000] + "... [TRUNCATED]";
return json;
}
return request?.ToString() ?? "";
}
}
@@ -0,0 +1,14 @@
using Mapster;
using ProtoBlogCategory = CMSMicroservice.Protobuf.Protos.BlogCategory;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class BlogCategoryProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// CreateBlogCategory: long → CreateBlogCategoryResponse
config.NewConfig<long, ProtoBlogCategory.CreateBlogCategoryResponse>()
.Map(dest => dest.Id, src => src);
}
}
@@ -0,0 +1,26 @@
using CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
using Google.Protobuf.WellKnownTypes;
using Mapster;
using ProtoBlogPost = CMSMicroservice.Protobuf.Protos.BlogPost;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class BlogPostProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// PublishBlogPost: Command result → Proto response
config.NewConfig<PublishBlogPostResult, ProtoBlogPost.PublishBlogPostResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.PublishedAt, src => src.PublishedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.PublishedAt.Value, DateTimeKind.Utc))
: null);
// ArchiveBlogPost: Command result → Proto response
config.NewConfig<ArchiveBlogPostResult, ProtoBlogPost.ArchiveBlogPostResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message);
}
}
@@ -24,9 +24,9 @@ public class CityProfile : IRegister
// Response: Application → Proto
config.NewConfig<AppCity.GetAllCitiesByFilterResponseDto, ProtoCity.GetAllCitiesByFilterResponse>()
.Map(dest => dest.MetaData, src => src.MetaData)
.Map(dest => dest.Models, src => src.Models);
.Map(dest => dest.Cities, src => src.Models);
config.NewConfig<AppCity.GetAllCitiesByFilterResponseModel, ProtoCity.GetAllCitiesByFilterResponseModel>()
config.NewConfig<AppCity.GetAllCitiesByFilterResponseModel, ProtoCity.CityDto>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.ExternalId, src => src.ExternalId)
.Map(dest => dest.Name, src => src.Name)
@@ -1,4 +1,5 @@
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetAllClubMemberships;
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembership;
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using Google.Protobuf.WellKnownTypes;
@@ -11,6 +12,37 @@ public class ClubMembershipProfile : IRegister
{
public void Register(TypeAdapterConfig config)
{
// =====================
// GetClubMembership
// =====================
// GetClubMembershipRequest -> GetClubMembershipQuery
config.NewConfig<GetClubMembershipRequest, GetClubMembershipQuery>()
.MapWith(src => new GetClubMembershipQuery
{
UserId = src.UserId
});
// ClubMembershipDto -> GetClubMembershipResponse
config.NewConfig<ClubMembershipDto, GetClubMembershipResponse>()
.MapWith(src => new GetClubMembershipResponse
{
Id = src.Id,
UserId = src.UserId,
PackageId = 0, // Not available
PackageName = string.Empty, // Not available
ActivationCode = string.Empty, // Not available
ActivatedAt = src.ActivatedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.ActivatedAt.Value, DateTimeKind.Utc))
: null,
ExpiresAt = null, // Not available
IsActive = src.IsActive,
Created = Timestamp.FromDateTimeOffset(src.Created),
Features = { }, // Empty for now
Status = src.IsActive ? "Active" : "Inactive",
DaysRemaining = 0 // Calculate if ExpiresAt is available
});
// =====================
// GetAllClubMemberships
// =====================
@@ -1,8 +1,12 @@
using System.Linq;
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Protobuf.Protos.Commission;
using Google.Protobuf.WellKnownTypes;
using Mapster;
@@ -124,5 +128,77 @@ public class CommissionProfile : IRegister
: null)
.Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.Created, src => Timestamp.FromDateTimeOffset(src.Created));
// GetMyCommissionPayouts Request Mapping
config.NewConfig<GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery>()
.Map(dest => dest.Status, src => src.Status != null && src.Status.Value >= 0
? (CommissionPayoutStatus?)src.Status.Value
: null)
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
.Map(dest => dest.PaginationState, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
PageSize = src.PageSize > 0 ? src.PageSize : 10
});
// GetMyCommissionPayouts Response Mapping
config.NewConfig<GetMyCommissionPayoutsResponseDto, GetMyCommissionPayoutsResponse>()
.Map(dest => dest.MetaData, src => src.MetaData != null
? new CustomerMetaData { TotalCount = src.MetaData.TotalCount }
: new CustomerMetaData())
.Map(dest => dest.Payouts, src => src.Models);
// CustomerCommissionPayoutModel Mapping
config.NewConfig<GetMyCommissionPayoutsResponseModel, CustomerCommissionPayoutModel>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
.Map(dest => dest.WeekDisplayName, src => src.WeekDisplayName)
.Map(dest => dest.BalancesEarned, src => src.BalancesEarned)
.Map(dest => dest.TotalAmount, src => src.TotalAmount)
.Map(dest => dest.AmountFormatted, src => src.AmountFormatted)
.Map(dest => dest.Status, src => (int)src.Status)
.Map(dest => dest.CalculatedDate, src => src.CalculatedDate.HasValue
? Timestamp.FromDateTime(src.CalculatedDate.Value.ToUniversalTime())
: null)
.Map(dest => dest.DatePersian, src => src.DatePersian);
// GetMyWeeklyBalances Request Mapping
config.NewConfig<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery>()
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : (long?)null)
.Map(dest => dest.OnlyActive, src => src.OnlyActive)
.Map(dest => dest.PaginationState, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
PageSize = src.PageSize > 0 ? src.PageSize : 10
});
// GetMyWeeklyBalances Response Mapping
config.NewConfig<GetUserWeeklyBalancesResponseDto, GetMyWeeklyBalancesResponse>()
.Map(dest => dest.MetaData, src => src.MetaData != null
? new CustomerMetaData { TotalCount = src.MetaData.TotalCount }
: new CustomerMetaData())
.Map(dest => dest.Balances, src => src.Models)
.Map(dest => dest.TotalLeftBalances, src => src.Models.Sum(m => m.LeftLegTotal))
.Map(dest => dest.TotalRightBalances, src => src.Models.Sum(m => m.RightLegTotal))
.Map(dest => dest.WeakerLeg, src =>
src.Models.Sum(m => m.LeftLegTotal) < src.Models.Sum(m => m.RightLegTotal)
? "Left"
: src.Models.Sum(m => m.LeftLegTotal) > src.Models.Sum(m => m.RightLegTotal)
? "Right"
: "Equal");
// CustomerWeeklyBalanceModel Mapping
config.NewConfig<GetUserWeeklyBalancesResponseModel, CustomerWeeklyBalanceModel>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
.Map(dest => dest.WeekDisplayName, src => src.WeekDisplayName)
.Map(dest => dest.LeftLegBalances, src => src.LeftLegTotal)
.Map(dest => dest.RightLegBalances, src => src.RightLegTotal)
.Map(dest => dest.TotalBalances, src => src.TotalBalances)
.Map(dest => dest.WeeklyPoolContribution, src => src.WeeklyPoolContribution)
.Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.CalculatedAt, src => src.CalculatedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedAt.Value, DateTimeKind.Utc))
: null);
}
}
@@ -0,0 +1,61 @@
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountCategories;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using Mapster;
using AppDiscountCategoryDto = CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountCategories.DiscountCategoryDto;
using ProtoDiscountCategoryDto = CMSMicroservice.Protobuf.Protos.DiscountCategory.DiscountCategoryDto;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class DiscountCategoryProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// Map from Application DTO to Proto Response
config.NewConfig<GetDiscountCategoriesResponseDto, GetDiscountCategoriesResponse>()
.MapWith(src => MapToResponse(src));
// Map individual category DTO
config.NewConfig<AppDiscountCategoryDto, ProtoDiscountCategoryDto>()
.MapWith(src => MapCategory(src));
}
private static GetDiscountCategoriesResponse MapToResponse(GetDiscountCategoriesResponseDto src)
{
var response = new GetDiscountCategoriesResponse();
if (src.Categories != null)
{
foreach (var category in src.Categories)
{
response.Categories.Add(MapCategory(category));
}
}
return response;
}
private static ProtoDiscountCategoryDto MapCategory(AppDiscountCategoryDto src)
{
var proto = new ProtoDiscountCategoryDto
{
Id = src.Id,
Name = src.Name ?? string.Empty,
Title = src.Title ?? string.Empty,
Description = src.Description,
ImagePath = src.ImagePath,
ParentCategoryId = src.ParentCategoryId,
SortOrder = src.SortOrder,
IsActive = src.IsActive,
ProductCount = src.ProductCount
};
// Recursively map children
if (src.Children != null)
{
foreach (var child in src.Children)
{
proto.Children.Add(MapCategory(child));
}
}
return proto;
}
}
@@ -0,0 +1,36 @@
using CMSMicroservice.Protobuf.Protos.Health;
using CMSMicroservice.Application.HealthCQ.Queries.GetSystemHealth;
using Google.Protobuf.WellKnownTypes;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class HealthProfile : IRegister
{
public void Register(TypeAdapterConfig config)
{
// DTO to Proto mappings
config.NewConfig<GetSystemHealthResponseDto, GetSystemHealthResponse>()
.Map(dest => dest.OverallHealthy, src => src.OverallHealthy)
.Map(dest => dest.Services, src => src.Services)
.Map(dest => dest.CheckedAt, src => Timestamp.FromDateTime(src.CheckedAt))
.Map(dest => dest.Version, src => src.Version)
.Map(dest => dest.Environment, src => src.Environment);
config.NewConfig<ServiceHealthDto, ServiceHealthModel>()
.Map(dest => dest.ServiceName, src => src.ServiceName)
.Map(dest => dest.Status, src => src.Status)
.Map(dest => dest.Description, src => src.Description)
.Map(dest => dest.ResponseTimeMs, src => src.ResponseTimeMs)
.Map(dest => dest.LastCheck, src => Timestamp.FromDateTime(src.LastCheck))
.Map(dest => dest.Details, src => src.Details);
config.NewConfig<HealthDetailDto, HealthDetail>()
.Map(dest => dest.Key, src => src.Key)
.Map(dest => dest.Value, src => src.Value)
.Map(dest => dest.Status, src => src.Status);
// Enum mappings
config.NewConfig<HealthStatusDto, HealthStatus>()
.Map(dest => dest, src => (HealthStatus)(int)src);
}
}
@@ -0,0 +1,484 @@
using CMSMicroservice.Application.WarehouseCQ.Commands.CreateWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Commands.UpdateWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Commands.DeleteWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Commands.SetDefaultWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Queries.GetWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Queries.GetAllWarehouses;
using CMSMicroservice.Application.InventoryItemCQ.Commands.UpdateInventoryItem;
using CMSMicroservice.Application.InventoryItemCQ.Commands.UpdateInventoryQuantity;
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReserveInventory;
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReleaseReservedInventory;
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReduceInventory;
using CMSMicroservice.Application.InventoryItemCQ.Commands.IncreaseInventory;
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetInventoryItem;
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetInventoryByProduct;
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetAllInventoryItems;
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetLowStockItems;
using CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
using CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovements;
using CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovementsByInventoryItem;
using CMSMicroservice.Protobuf.Protos.Inventory;
using CMSMicroservice.Domain.Enums;
using Google.Protobuf.WellKnownTypes;
using Mapster;
using DomainStockMovementType = CMSMicroservice.Domain.Enums.StockMovementType;
using ProtoStockMovementType = CMSMicroservice.Protobuf.Protos.Inventory.StockMovementType;
using DomainProductType = CMSMicroservice.Domain.Enums.ProductType;
using ProtoProductType = CMSMicroservice.Protobuf.Protos.Inventory.ProductType;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class InventoryProfile : IRegister
{
public void Register(TypeAdapterConfig config)
{
// ==================== Warehouse Mappings ====================
// CreateWarehouse: Proto -> Command
config.NewConfig<CreateWarehouseRequest, CreateWarehouseCommand>()
.Map(dest => dest.Name, src => src.Name)
.Map(dest => dest.Code, src => src.Code)
.Map(dest => dest.Address, src => src.Address)
.Map(dest => dest.IsDefault, src => src.IsDefault)
.Map(dest => dest.IsActive, src => true); // Proto ندارد
// CreateWarehouse: ResponseDto -> Proto Response
config.NewConfig<CreateWarehouseResponseDto, CreateWarehouseResponse>()
.Map(dest => dest.Id, src => src.Id);
// UpdateWarehouse: Proto -> Command
config.NewConfig<UpdateWarehouseRequest, UpdateWarehouseCommand>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.Name, src => src.Name)
.Map(dest => dest.Code, src => src.Code)
.Map(dest => dest.Address, src => src.Address)
.Map(dest => dest.IsActive, src => src.IsActive);
// DeleteWarehouse: Proto -> Command
config.NewConfig<DeleteWarehouseRequest, DeleteWarehouseCommand>()
.Map(dest => dest.Id, src => src.Id);
// SetDefaultWarehouse: Proto -> Command
config.NewConfig<SetDefaultWarehouseRequest, SetDefaultWarehouseCommand>()
.Map(dest => dest.Id, src => src.Id);
// GetWarehouse Query: Proto -> Query
config.NewConfig<GetWarehouseRequest, GetWarehouseQuery>()
.MapWith(src => new GetWarehouseQuery(src.Id));
// GetWarehouse Response: ResponseDto -> Proto (با WarehouseDto)
config.NewConfig<GetWarehouseResponseDto, GetWarehouseResponse>()
.Map(dest => dest.Warehouse, src => new WarehouseDto
{
Id = src.Id,
Name = src.Name,
Code = src.Code,
Address = src.Address ?? string.Empty,
IsDefault = src.IsDefault,
IsActive = src.IsActive,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc)),
LastModified = Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc))
});
// GetAllWarehouses Query: Proto -> Query
config.NewConfig<GetAllWarehousesRequest, GetAllWarehousesQuery>()
.MapWith(src => new GetAllWarehousesQuery());
// GetAllWarehouses Response: ResponseDto -> Proto
config.NewConfig<GetAllWarehousesResponseDto, GetAllWarehousesResponse>()
.Map(dest => dest.TotalCount, src => src.TotalCount)
.AfterMapping((src, dest) =>
{
dest.Warehouses.Clear();
foreach (var warehouse in src.Warehouses)
{
dest.Warehouses.Add(new WarehouseDto
{
Id = warehouse.Id,
Name = warehouse.Name,
Code = warehouse.Code,
Address = warehouse.Address ?? string.Empty,
IsDefault = warehouse.IsDefault,
IsActive = warehouse.IsActive,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc)),
LastModified = Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc))
});
}
});
// ==================== Inventory Item Query Mappings ====================
// GetInventoryItem: Proto -> Query
config.NewConfig<GetInventoryItemRequest, GetInventoryItemQuery>()
.MapWith(src => new GetInventoryItemQuery(src.Id));
// GetInventoryItem Response: ResponseDto -> Proto (با InventoryItemDto)
config.NewConfig<GetInventoryItemResponseDto, GetInventoryItemResponse>()
.Map(dest => dest.Item, src => MapToInventoryItemDto(src));
// GetInventoryByProduct: Proto -> Query
config.NewConfig<GetInventoryByProductRequest, GetInventoryByProductQuery>()
.Map(dest => dest.ProductId, src => src.ProductId)
.Map(dest => dest.ProductType, src => MapToDomainProductType(src.ProductType));
// GetInventoryByProduct Response: ResponseDto -> Proto
config.NewConfig<GetInventoryByProductResponseDto, GetInventoryByProductResponse>()
.Map(dest => dest.Item, src => MapToInventoryItemDto(src));
// GetAllInventoryItems: Proto -> Query
config.NewConfig<GetAllInventoryItemsRequest, GetAllInventoryItemsQuery>()
.Map(dest => dest.WarehouseId, src => src.WarehouseId != null ? (long?)src.WarehouseId.Value : null)
.Map(dest => dest.ProductType, src => src.ProductType != ProtoProductType.Unspecified
? (DomainProductType?)MapToDomainProductType(src.ProductType) : null)
.Map(dest => dest.SearchTerm, src => src.Search)
.Map(dest => dest.Skip, src => src.Page > 0 ? (src.Page - 1) * src.PageSize : 0)
.Map(dest => dest.Take, src => src.PageSize > 0 ? src.PageSize : 50);
// GetAllInventoryItems Response: ResponseDto -> Proto
config.NewConfig<GetAllInventoryItemsResponseDto, GetAllInventoryItemsResponse>()
.Map(dest => dest.TotalCount, src => src.TotalCount)
.AfterMapping((src, dest) =>
{
dest.Items.Clear();
foreach (var item in src.Items)
{
dest.Items.Add(MapListItemToInventoryItemDto(item));
}
});
// GetLowStockItems: Proto -> Query
config.NewConfig<GetLowStockItemsRequest, GetLowStockItemsQuery>()
.Map(dest => dest.WarehouseId, src => src.WarehouseId != null ? (long?)src.WarehouseId.Value : null)
.Map(dest => dest.Count, src => src.PageSize > 0 ? src.PageSize : 50);
// GetLowStockItems Response: ResponseDto -> Proto
config.NewConfig<GetLowStockItemsResponseDto, GetLowStockItemsResponse>()
.Map(dest => dest.TotalCount, src => src.TotalCount)
.AfterMapping((src, dest) =>
{
dest.Items.Clear();
foreach (var item in src.Items)
{
dest.Items.Add(MapLowStockItemToInventoryItemDto(item));
}
});
// UpdateInventorySettings: Proto -> UpdateInventoryItemCommand
config.NewConfig<UpdateInventorySettingsRequest, UpdateInventoryItemCommand>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.MinimumStock, src => (int?)src.LowStockThreshold)
.Map(dest => dest.MaximumStock, src => (int?)src.MaxStockLevel)
.Map(dest => dest.ReorderPoint, src => (int?)src.ReorderPoint);
// ==================== Stock Operation Mappings ====================
// AddStock: Proto -> IncreaseInventoryCommand
// نکته: این نیاز به lookup در Service داره چون Proto از ProductId استفاده میکنه
config.NewConfig<AddStockRequest, IncreaseInventoryCommand>()
.Map(dest => dest.Quantity, src => src.Quantity)
.Map(dest => dest.ReferenceNumber, src => src.ReferenceNumber)
.Map(dest => dest.Note, src => src.Note);
// Id باید در Service تنظیم بشه
// AdjustStock: Proto -> UpdateInventoryQuantityCommand
config.NewConfig<AdjustStockRequest, UpdateInventoryQuantityCommand>()
.Map(dest => dest.NewQuantity, src => src.NewQuantity)
.Map(dest => dest.Note, src => src.Reason)
.Map(dest => dest.ReferenceNumber, src => src.ReferenceNumber);
// Id باید در Service تنظیم بشه
// ReserveStock: Proto -> ReserveInventoryCommand
config.NewConfig<ReserveStockRequest, ReserveInventoryCommand>()
.Map(dest => dest.Quantity, src => src.Quantity)
.Map(dest => dest.OrderId, src => src.OrderId != null ? (long?)src.OrderId.Value : null)
.Map(dest => dest.DiscountOrderId, src => src.DiscountOrderId != null ? (long?)src.DiscountOrderId.Value : null);
// Id باید در Service تنظیم بشه
// ReleaseReservation: Proto -> ReleaseReservedInventoryCommand
config.NewConfig<ReleaseReservationRequest, ReleaseReservedInventoryCommand>()
.Map(dest => dest.Quantity, src => src.Quantity)
.Map(dest => dest.OrderId, src => src.OrderId != null ? (long?)src.OrderId.Value : null)
.Map(dest => dest.DiscountOrderId, src => src.DiscountOrderId != null ? (long?)src.DiscountOrderId.Value : null);
// Id باید در Service تنظیم بشه
// ConfirmSale: Proto -> ReduceInventoryCommand
config.NewConfig<ConfirmSaleRequest, ReduceInventoryCommand>()
.Map(dest => dest.Quantity, src => src.Quantity)
.Map(dest => dest.OrderId, src => src.OrderId != null ? (long?)src.OrderId.Value : null)
.Map(dest => dest.DiscountOrderId, src => src.DiscountOrderId != null ? (long?)src.DiscountOrderId.Value : null)
.Map(dest => dest.FromReserved, src => src.FromReservation);
// Id باید در Service تنظیم بشه
// ProcessReturn: Proto -> IncreaseInventoryCommand
config.NewConfig<ProcessReturnRequest, IncreaseInventoryCommand>()
.Map(dest => dest.Quantity, src => src.Quantity)
.Map(dest => dest.Note, src => src.Reason);
// Id باید در Service تنظیم بشه
// RecordLoss: Proto -> CreateStockMovementCommand
config.NewConfig<RecordLossRequest, CreateStockMovementCommand>()
.Map(dest => dest.Quantity, src => src.Quantity)
.Map(dest => dest.MovementType, src => MapToDomainStockMovementType(src.LossType))
.Map(dest => dest.Note, src => src.Reason)
.Map(dest => dest.ReferenceNumber, src => src.ReferenceNumber);
// InventoryItemId باید در Service تنظیم بشه
// ==================== Stock Movement Query Mappings ====================
// GetStockMovements: Proto -> Query
config.NewConfig<GetStockMovementsRequest, GetStockMovementsQuery>()
.Map(dest => dest.InventoryItemId, src => src.InventoryItemId != null ? (long?)src.InventoryItemId.Value : null)
.Map(dest => dest.ProductId, src => src.ProductId != null ? (long?)src.ProductId.Value : null)
.Map(dest => dest.ProductType, src => src.ProductType != ProtoProductType.Unspecified
? (DomainProductType?)MapToDomainProductType(src.ProductType) : null)
.Map(dest => dest.MovementType, src => src.MovementType != ProtoStockMovementType.MovementTypeUnspecified
? (DomainStockMovementType?)MapToDomainStockMovementType(src.MovementType) : null)
.Map(dest => dest.StartDate, src => src.FromDate != null ? (DateTime?)src.FromDate.ToDateTime() : null)
.Map(dest => dest.EndDate, src => src.ToDate != null ? (DateTime?)src.ToDate.ToDateTime() : null)
.Map(dest => dest.Skip, src => src.Page > 0 ? (src.Page - 1) * src.PageSize : 0)
.Map(dest => dest.Take, src => src.PageSize > 0 ? src.PageSize : 50);
// GetStockMovements Response: ResponseDto -> Proto
config.NewConfig<GetStockMovementsResponseDto, GetStockMovementsResponse>()
.Map(dest => dest.TotalCount, src => src.TotalCount)
.AfterMapping((src, dest) =>
{
dest.Movements.Clear();
foreach (var movement in src.Movements)
{
dest.Movements.Add(MapToStockMovementDto(movement));
}
});
// GetStockMovementsByInventoryItem: Proto -> Query
config.NewConfig<GetStockMovementsByInventoryItemRequest, GetStockMovementsByInventoryItemQuery>()
.Map(dest => dest.InventoryItemId, src => src.InventoryItemId)
.Map(dest => dest.Skip, src => src.Page > 0 ? (src.Page - 1) * src.PageSize : 0)
.Map(dest => dest.Take, src => src.PageSize > 0 ? src.PageSize : 50);
// GetStockMovementsByInventoryItem Response: ResponseDto -> Proto
config.NewConfig<GetStockMovementsByInventoryItemResponseDto, GetStockMovementsByInventoryItemResponse>()
.Map(dest => dest.TotalCount, src => src.TotalCount)
.AfterMapping((src, dest) =>
{
dest.Movements.Clear();
foreach (var movement in src.Movements)
{
dest.Movements.Add(MapHistoryToStockMovementDto(movement));
}
});
}
// ==================== Helper Methods ====================
private static InventoryItemDto MapToInventoryItemDto(GetInventoryItemResponseDto src)
{
return new InventoryItemDto
{
Id = src.Id,
ProductId = src.ProductId,
DiscountProductId = src.DiscountProductId,
ProductType = MapToProtoProductType(src.ProductType),
Quantity = src.Quantity,
ReservedQuantity = src.ReservedQuantity,
AvailableQuantity = src.AvailableQuantity,
LowStockThreshold = src.LowStockThreshold,
ReorderPoint = 0, // Entity ندارد
MaxStockLevel = src.MaxStockLevel,
LastRestockedAt = src.LastRestockedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.LastRestockedAt.Value, DateTimeKind.Utc))
: null,
LastSoldAt = src.LastSoldAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.LastSoldAt.Value, DateTimeKind.Utc))
: null,
WarehouseId = src.WarehouseId,
WarehouseName = src.WarehouseName ?? string.Empty,
ProductTitle = src.ProductTitle ?? string.Empty,
ProductPrice = src.ProductPrice.HasValue ? (long)src.ProductPrice.Value : 0,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc))
};
}
private static InventoryItemDto MapToInventoryItemDto(GetInventoryByProductResponseDto src)
{
return new InventoryItemDto
{
Id = src.Id,
ProductId = src.ProductId,
DiscountProductId = src.DiscountProductId,
ProductType = MapToProtoProductType(src.ProductType),
Quantity = src.Quantity,
ReservedQuantity = src.ReservedQuantity,
AvailableQuantity = src.AvailableQuantity,
LowStockThreshold = src.LowStockThreshold,
ReorderPoint = 0,
MaxStockLevel = src.MaxStockLevel,
LastRestockedAt = src.LastRestockedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.LastRestockedAt.Value, DateTimeKind.Utc))
: null,
LastSoldAt = src.LastSoldAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.LastSoldAt.Value, DateTimeKind.Utc))
: null,
WarehouseId = src.WarehouseId,
WarehouseName = src.WarehouseName ?? string.Empty,
ProductTitle = src.ProductTitle ?? string.Empty,
ProductPrice = src.ProductPrice.HasValue ? (long)src.ProductPrice.Value : 0,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc))
};
}
private static InventoryItemDto MapListItemToInventoryItemDto(InventoryItemListDto src)
{
return new InventoryItemDto
{
Id = src.Id,
ProductId = src.ProductId,
DiscountProductId = src.DiscountProductId,
ProductType = MapToProtoProductType(src.ProductType),
Quantity = src.Quantity,
ReservedQuantity = src.ReservedQuantity,
AvailableQuantity = src.AvailableQuantity,
LowStockThreshold = src.LowStockThreshold,
ReorderPoint = 0,
MaxStockLevel = src.MaxStockLevel,
LastRestockedAt = src.LastRestockedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.LastRestockedAt.Value, DateTimeKind.Utc))
: null,
LastSoldAt = src.LastSoldAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.LastSoldAt.Value, DateTimeKind.Utc))
: null,
WarehouseId = src.WarehouseId,
WarehouseName = src.WarehouseName ?? string.Empty,
ProductTitle = src.ProductTitle ?? string.Empty,
ProductPrice = src.ProductPrice.HasValue ? (long)src.ProductPrice.Value : 0,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc))
};
}
private static InventoryItemDto MapLowStockItemToInventoryItemDto(LowStockItemDto src)
{
return new InventoryItemDto
{
Id = src.Id,
ProductId = src.ProductId,
DiscountProductId = src.DiscountProductId,
ProductType = MapToProtoProductType(src.ProductType),
Quantity = src.Quantity,
ReservedQuantity = src.ReservedQuantity,
AvailableQuantity = src.AvailableQuantity,
LowStockThreshold = src.LowStockThreshold,
ReorderPoint = 0,
MaxStockLevel = 0,
LastRestockedAt = null,
LastSoldAt = null,
WarehouseId = src.WarehouseId,
WarehouseName = src.WarehouseName ?? string.Empty,
ProductTitle = src.ProductTitle ?? string.Empty,
ProductPrice = 0,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc))
};
}
private static StockMovementDto MapToStockMovementDto(StockMovementListDto src)
{
return new StockMovementDto
{
Id = src.Id,
InventoryItemId = src.InventoryItemId,
MovementType = MapToProtoStockMovementType(src.MovementType),
Quantity = src.Quantity,
QuantityBefore = src.QuantityBefore,
QuantityAfter = src.QuantityAfter,
OrderId = src.OrderId,
DiscountOrderId = src.DiscountOrderId,
ReferenceNumber = src.ReferenceNumber ?? string.Empty,
Note = src.Note ?? string.Empty,
PerformedByUserId = src.PerformedByUserId,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc)),
ProductTitle = src.ProductTitle ?? string.Empty
};
}
private static StockMovementDto MapHistoryToStockMovementDto(StockMovementHistoryDto src)
{
return new StockMovementDto
{
Id = src.Id,
InventoryItemId = 0, // Not available in history DTO
MovementType = MapToProtoStockMovementType(src.MovementType),
Quantity = src.Quantity,
QuantityBefore = src.QuantityBefore,
QuantityAfter = src.QuantityAfter,
OrderId = src.OrderId,
DiscountOrderId = src.DiscountOrderId,
ReferenceNumber = src.ReferenceNumber ?? string.Empty,
Note = src.Note ?? string.Empty,
PerformedByUserId = src.PerformedByUserId,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(src.Created, DateTimeKind.Utc)),
ProductTitle = string.Empty
};
}
// ==================== Enum Mapping Methods ====================
private static DomainProductType MapToDomainProductType(ProtoProductType protoType)
{
return protoType switch
{
ProtoProductType.RegularProduct => DomainProductType.RegularProduct,
ProtoProductType.DiscountProduct => DomainProductType.DiscountProduct,
_ => DomainProductType.RegularProduct
};
}
private static ProtoProductType MapToProtoProductType(DomainProductType domainType)
{
return domainType switch
{
DomainProductType.RegularProduct => ProtoProductType.RegularProduct,
DomainProductType.DiscountProduct => ProtoProductType.DiscountProduct,
_ => ProtoProductType.Unspecified
};
}
private static DomainStockMovementType MapToDomainStockMovementType(ProtoStockMovementType protoType)
{
return protoType switch
{
ProtoStockMovementType.InitialStock => DomainStockMovementType.InitialStock,
ProtoStockMovementType.Restock => DomainStockMovementType.Restock,
ProtoStockMovementType.Return => DomainStockMovementType.Return,
ProtoStockMovementType.Sale => DomainStockMovementType.Sale,
ProtoStockMovementType.AdjustmentIncrease => DomainStockMovementType.AdjustmentPlus,
ProtoStockMovementType.AdjustmentDecrease => DomainStockMovementType.AdjustmentMinus,
ProtoStockMovementType.Reserved => DomainStockMovementType.Reserved,
ProtoStockMovementType.Released => DomainStockMovementType.Released,
ProtoStockMovementType.Loss => DomainStockMovementType.Lost,
ProtoStockMovementType.Damaged => DomainStockMovementType.Damaged,
ProtoStockMovementType.Expired => DomainStockMovementType.Damaged, // Domain ندارد Expired
ProtoStockMovementType.TransferOut => DomainStockMovementType.TransferOut,
ProtoStockMovementType.TransferIn => DomainStockMovementType.TransferIn,
_ => DomainStockMovementType.AdjustmentPlus
};
}
private static ProtoStockMovementType MapToProtoStockMovementType(DomainStockMovementType domainType)
{
return domainType switch
{
DomainStockMovementType.InitialStock => ProtoStockMovementType.InitialStock,
DomainStockMovementType.Restock => ProtoStockMovementType.Restock,
DomainStockMovementType.Return => ProtoStockMovementType.Return,
DomainStockMovementType.Sale => ProtoStockMovementType.Sale,
DomainStockMovementType.AdjustmentPlus => ProtoStockMovementType.AdjustmentIncrease,
DomainStockMovementType.AdjustmentMinus => ProtoStockMovementType.AdjustmentDecrease,
DomainStockMovementType.Reserved => ProtoStockMovementType.Reserved,
DomainStockMovementType.Released => ProtoStockMovementType.Released,
DomainStockMovementType.Lost => ProtoStockMovementType.Loss,
DomainStockMovementType.Damaged => ProtoStockMovementType.Damaged,
DomainStockMovementType.TransferOut => ProtoStockMovementType.TransferOut,
DomainStockMovementType.TransferIn => ProtoStockMovementType.TransferIn,
_ => ProtoStockMovementType.MovementTypeUnspecified
};
}
}
@@ -1,93 +0,0 @@
using CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductPrices;
using CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductStock;
using CMSMicroservice.Application.ProductsCQ.Commands.ToggleProductStatus;
using CMSMicroservice.Application.ProductsCQ.Queries.GetLowStockProducts;
using CMSMicroservice.Protobuf.Protos.Products;
using ProtoProductPriceUpdate = CMSMicroservice.Protobuf.Protos.Products.ProductPriceUpdate;
using AppProductPriceUpdate = CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductPrices.ProductPriceUpdate;
using ProtoProductStockUpdate = CMSMicroservice.Protobuf.Protos.Products.ProductStockUpdate;
using AppProductStockUpdate = CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductStock.ProductStockUpdate;
using ProtoStockUpdateType = CMSMicroservice.Protobuf.Protos.Products.StockUpdateType;
using AppStockUpdateType = CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductStock.StockUpdateType;
using System.Linq;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class ProductsProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// BulkUpdateProductPrices mappings
config.NewConfig<BulkUpdateProductPricesRequest, BulkUpdateProductPricesCommand>()
.Map(dest => dest.Products, src => src.Products);
config.NewConfig<ProtoProductPriceUpdate, AppProductPriceUpdate>()
.Map(dest => dest.ProductId, src => src.ProductId)
.Map(dest => dest.NewPrice, src => src.NewPrice)
.Map(dest => dest.NewDiscount, src => src.NewDiscount != null ? (int?)src.NewDiscount.Value : null)
.Map(dest => dest.NewClubDiscountPercent, src => src.NewClubDiscountPercent != null ? (int?)src.NewClubDiscountPercent.Value : null);
config.NewConfig<BulkUpdateProductPricesResponseDto, BulkUpdateProductPricesResponse>()
.Map(dest => dest.Total, src => src.UpdatedCount + src.FailedCount)
.Map(dest => dest.Succeeded, src => src.UpdatedCount)
.Map(dest => dest.Failed, src => src.FailedCount)
.Map(dest => dest.Errors, src => src.Errors.Select((msg, idx) => new BulkOperationError
{
ProductId = 0, // We don't have the ID in the error message
ErrorMessage = msg
}).ToList());
// BulkUpdateProductStock mappings
config.NewConfig<BulkUpdateProductStockRequest, BulkUpdateProductStockCommand>()
.Map(dest => dest.Products, src => src.Products)
.Map(dest => dest.UpdateType, src => (AppStockUpdateType)src.UpdateType);
config.NewConfig<ProtoProductStockUpdate, AppProductStockUpdate>()
.Map(dest => dest.ProductId, src => src.ProductId)
.Map(dest => dest.Quantity, src => src.Quantity);
config.NewConfig<BulkUpdateProductStockResponseDto, BulkUpdateProductStockResponse>()
.Map(dest => dest.Total, src => src.UpdatedCount + src.FailedCount)
.Map(dest => dest.Succeeded, src => src.UpdatedCount)
.Map(dest => dest.Failed, src => src.FailedCount)
.Map(dest => dest.Errors, src => src.Errors.Select(msg => new BulkOperationError
{
ProductId = 0,
ErrorMessage = msg
}).ToList());
// GetLowStockProducts mappings
config.NewConfig<GetLowStockProductsRequest, GetLowStockProductsQuery>()
.Map(dest => dest.Threshold, src => src.Threshold)
.Map(dest => dest.PageIndex, src => src.PageIndex)
.Map(dest => dest.PageSize, src => src.PageSize)
.Map(dest => dest.IsClubExclusive, src => src.IsClubExclusive != null ? (bool?)src.IsClubExclusive.Value : null);
config.NewConfig<GetLowStockProductsResponseDto, GetLowStockProductsResponse>()
.Map(dest => dest.MetaData, src => src.MetaData)
.Map(dest => dest.Products, src => src.Products);
config.NewConfig<LowStockProductDto, LowStockProduct>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.Title, src => src.Title)
.Map(dest => dest.RemainingCount, src => src.RemainingCount)
.Map(dest => dest.Price, src => src.Price)
.Map(dest => dest.IsClubExclusive, src => src.IsClubExclusive);
// ToggleProductStatus mappings
config.NewConfig<ToggleProductStatusRequest, ToggleProductStatusCommand>()
.Map(dest => dest.ProductIds, src => src.ProductIds)
.Map(dest => dest.Enable, src => src.Enable)
.Map(dest => dest.DefaultStock, src => src.DefaultStock);
config.NewConfig<ToggleProductStatusResponseDto, ToggleProductStatusResponse>()
.Map(dest => dest.Total, src => src.UpdatedCount + src.FailedCount)
.Map(dest => dest.Succeeded, src => src.UpdatedCount)
.Map(dest => dest.Failed, src => src.FailedCount)
.Map(dest => dest.Errors, src => src.Errors.Select(msg => new BulkOperationError
{
ProductId = 0,
ErrorMessage = msg
}).ToList());
}
}
@@ -1,44 +0,0 @@
using CMSMicroservice.Application.UserOrderCQ.Commands.UpdateOrderStatus;
using CMSMicroservice.Application.UserOrderCQ.Commands.ApplyDiscountToOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange;
using CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV;
using Google.Protobuf.WellKnownTypes;
namespace CMSMicroservice.WebApi.Common.Mappings;
public class UserOrderProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
config.NewConfig<Protobuf.Protos.UserOrder.GetAllUserOrderByFilterRequest, Application.UserOrderCQ.Queries.GetAllUserOrderByFilter.GetAllUserOrderByFilterQuery>()
.IgnoreIf((src, dest) => src.Filter == null || !src.Filter.HasPaymentStatus, dest => dest.Filter.PaymentStatus)
.IgnoreIf((src, dest) => src.Filter == null || !src.Filter.HasPaymentMethod, dest => dest.Filter.PaymentMethod)
.IgnoreIf((src, dest) => src.Filter == null || !src.Filter.HasDeliveryStatus, dest => dest.Filter.DeliveryStatus);
// UpdateOrderStatus
config.NewConfig<Protobuf.Protos.UserOrder.UpdateOrderStatusRequest, UpdateOrderStatusCommand>();
config.NewConfig<UpdateOrderStatusResponseDto, Protobuf.Protos.UserOrder.UpdateOrderStatusResponse>();
// GetOrdersByDateRange
config.NewConfig<Protobuf.Protos.UserOrder.GetOrdersByDateRangeRequest, GetOrdersByDateRangeQuery>()
.Map(dest => dest.StartDate, src => src.StartDate.ToDateTime())
.Map(dest => dest.EndDate, src => src.EndDate.ToDateTime())
.Map(dest => dest.Status, src => src.Status != null ? (int?)src.Status.Value : null)
.Map(dest => dest.UserId, src => src.UserId != null ? (long?)src.UserId.Value : null);
config.NewConfig<GetOrdersByDateRangeResponseDto, Protobuf.Protos.UserOrder.GetOrdersByDateRangeResponse>()
.Map(dest => dest.Orders, src => src.Orders);
config.NewConfig<OrderSummaryDto, Protobuf.Protos.UserOrder.OrderSummaryDto>()
.Map(dest => dest.CreatedAt, src => Timestamp.FromDateTime(src.Created.ToUniversalTime()));
// ApplyDiscountToOrder
config.NewConfig<Protobuf.Protos.UserOrder.ApplyDiscountToOrderRequest, ApplyDiscountToOrderCommand>();
config.NewConfig<ApplyDiscountToOrderResponseDto, Protobuf.Protos.UserOrder.ApplyDiscountToOrderResponse>();
// CalculateOrderPV
config.NewConfig<Protobuf.Protos.UserOrder.CalculateOrderPVRequest, CalculateOrderPVQuery>();
config.NewConfig<CalculateOrderPVResponseDto, Protobuf.Protos.UserOrder.CalculateOrderPVResponse>();
config.NewConfig<ProductPVDto, Protobuf.Protos.UserOrder.ProductPVDto>();
}
}
@@ -0,0 +1,146 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Controllers;
/// <summary>
/// Callback endpoint for payment gateways (ZarinPal, etc.)
/// درگاه پرداخت بعد از پرداخت (یا لغو) کاربر را به اینجا redirect می‌کند
/// </summary>
[ApiController]
[AllowAnonymous] // کاربر از درگاه بانک برمی‌گردد — JWT ندارد
[ApiExplorerSettings(GroupName = "cms")]
public class PaymentCallbackController : ControllerBase
{
private readonly ISender _sender;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IApplicationDbContext _context;
private readonly IConfiguration _configuration;
private readonly ILogger<PaymentCallbackController> _logger;
public PaymentCallbackController(
ISender sender,
IPaymentGatewayService paymentGateway,
IApplicationDbContext context,
IConfiguration configuration,
ILogger<PaymentCallbackController> logger)
{
_sender = sender;
_paymentGateway = paymentGateway;
_context = context;
_configuration = configuration;
_logger = logger;
}
/// <summary>
/// Callback برای پرداخت سفارش فروشگاه تخفیفی
/// زرین‌پال کاربر را با Authority و Status به این endpoint برمی‌گرداند
/// </summary>
[HttpGet("/api/payment/discount-order/callback")]
public async Task<IActionResult> DiscountOrderCallback(
[FromQuery] long orderId,
[FromQuery(Name = "Authority")] string? authority,
[FromQuery(Name = "Status")] string? status,
CancellationToken cancellationToken)
{
var frontOfficeBaseUrl = _configuration["FrontOfficeBaseUrl"] ?? "https://localhost:5268";
_logger.LogInformation(
"Payment callback received: OrderId={OrderId}, Authority={Authority}, Status={Status}",
orderId, authority, status);
try
{
// پیدا کردن سفارش و تراکنش
var order = await _context.DiscountOrders
.Include(o => o.OrderDetails)
.FirstOrDefaultAsync(o => o.Id == orderId, cancellationToken);
if (order == null)
{
_logger.LogError("Payment callback: Order #{OrderId} not found", orderId);
return Redirect($"{frontOfficeBaseUrl}/discount-store/orders?error=order-not-found");
}
var transaction = order.TransactionId.HasValue
? await _context.Transactions.FirstOrDefaultAsync(
t => t.Id == order.TransactionId.Value, cancellationToken)
: null;
// تأیید پرداخت از درگاه
bool paymentSuccess = false;
string? refId = null;
if (string.Equals(status, "OK", StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(authority))
{
// Verify با مبلغ از دیتابیس (تومان)
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
authority,
status!,
order.GatewayAmountPaid, // مبلغ به تومان
cancellationToken);
paymentSuccess = verifyResult.IsSuccess;
refId = verifyResult.TrackingCode ?? verifyResult.RefId;
// آپدیت PaymentTransaction با نتیجه verify
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == authority, cancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
await _context.SaveChangesAsync(cancellationToken);
}
_logger.LogInformation(
"Payment verification for Order #{OrderId}: Success={Success}, RefId={RefId}, Message={Message}",
orderId, paymentSuccess, refId, verifyResult.Message);
}
else
{
_logger.LogWarning("Payment cancelled by user for Order #{OrderId}", orderId);
}
// تکمیل سفارش از طریق CQRS
var completeResult = await _sender.Send(new CompleteOrderPaymentCommand
{
OrderId = orderId,
TransactionId = transaction?.Id ?? 0,
PaymentSuccess = paymentSuccess,
RefId = refId
}, cancellationToken);
// Redirect به FrontOffice
if (paymentSuccess && completeResult.Success)
{
_logger.LogInformation("Payment completed successfully for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=success");
}
else
{
_logger.LogWarning("Payment failed for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=failed");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment callback error for Order #{OrderId}", orderId);
return Redirect(
$"{frontOfficeBaseUrl}/discount-store/order/{orderId}?payment=error");
}
}
}
@@ -0,0 +1,150 @@
using System.IO;
using System.Net.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Controllers;
/// <summary>
/// سرویس عمومی سرو تصاویر — فایل‌ها مستقیماً از پوشه Uploads سرو می‌شوند.
/// اگر فایل محلی وجود نداشت، از FMS قدیمی (dl.afrino.co) دانلود و کش می‌شود.
/// </summary>
[ApiController]
[AllowAnonymous]
[ApiExplorerSettings(GroupName = "cms")]
public class UploadsController : ControllerBase
{
private readonly string _uploadRoot;
private readonly string _fmsBaseUrl;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<UploadsController> _logger;
private readonly FileExtensionContentTypeProvider _contentTypeProvider = new();
// حداکثر طول مسیر مجاز (جلوگیری از path traversal)
private const int MaxPathLength = 500;
public UploadsController(
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
ILogger<UploadsController> logger)
{
_httpClientFactory = httpClientFactory;
_logger = logger;
_uploadRoot = configuration["FileStorage:UploadPath"]
?? Path.Combine(AppContext.BaseDirectory, "Uploads");
_fmsBaseUrl = configuration["FMS:Address"]?.TrimEnd('/') ?? string.Empty;
}
/// <summary>
/// سرو عمومی فایل از پوشه Uploads.
/// اگر فایل محلی وجود نداشت و FMS تنظیم شده باشد، از FMS دانلود و کش می‌شود.
/// </summary>
/// <param name="path">مسیر نسبی فایل (مثلاً blog/image.jpg)</param>
[HttpGet("uploads/{**path}")]
[ResponseCache(Duration = 86400, Location = ResponseCacheLocation.Any)] // کش مرورگر ۲۴ ساعت
public async Task<IActionResult> GetFile(string path)
{
// ── اعتبارسنجی مسیر ──
if (string.IsNullOrWhiteSpace(path) || path.Length > MaxPathLength)
return BadRequest("مسیر نامعتبر");
// جلوگیری از path traversal
if (path.Contains("..") || path.Contains('\\'))
return BadRequest("مسیر نامعتبر");
var sanitizedPath = path.TrimStart('/');
var fullPath = Path.GetFullPath(Path.Combine(_uploadRoot, sanitizedPath));
// اطمینان از اینکه مسیر درون _uploadRoot باقی می‌ماند
if (!fullPath.StartsWith(Path.GetFullPath(_uploadRoot), StringComparison.OrdinalIgnoreCase))
return BadRequest("مسیر نامعتبر");
// ── سرو فایل محلی ──
if (System.IO.File.Exists(fullPath))
return ServeFile(fullPath);
// ── Fallback: دانلود از FMS قدیمی ──
if (string.IsNullOrWhiteSpace(_fmsBaseUrl))
{
_logger.LogWarning("File not found locally and no FMS configured: {Path}", sanitizedPath);
return NotFound();
}
var downloaded = await TryDownloadFromFmsAsync(sanitizedPath, fullPath);
if (downloaded)
{
_logger.LogInformation("Downloaded and cached from FMS: {Path}", sanitizedPath);
return ServeFile(fullPath);
}
return NotFound();
}
// ────────────────────────────────────────────────────
// سرو فایل با Content-Type مناسب
// ────────────────────────────────────────────────────
private IActionResult ServeFile(string fullPath)
{
if (!_contentTypeProvider.TryGetContentType(fullPath, out var contentType))
contentType = "application/octet-stream";
var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
return File(stream, contentType, enableRangeProcessing: true);
}
// ────────────────────────────────────────────────────
// دانلود از FMS قدیمی و ذخیره محلی
// ────────────────────────────────────────────────────
private async Task<bool> TryDownloadFromFmsAsync(string relativePath, string localPath)
{
try
{
var fmsUrl = $"{_fmsBaseUrl}/{relativePath}";
_logger.LogInformation("Attempting FMS download: {Url}", fmsUrl);
using var client = _httpClientFactory.CreateClient("FMS");
using var response = await client.GetAsync(fmsUrl, HttpCompletionOption.ResponseHeadersRead);
if (!response.IsSuccessStatusCode)
{
_logger.LogWarning("FMS returned {Status} for {Url}", response.StatusCode, fmsUrl);
return false;
}
// بررسی Content-Type — فقط فایل‌های تصویری/مجاز
var mediaType = response.Content.Headers.ContentType?.MediaType ?? string.Empty;
if (!IsAllowedMediaType(mediaType))
{
_logger.LogWarning("FMS returned disallowed content type {Type} for {Url}", mediaType, fmsUrl);
return false;
}
// ذخیره روی دیسک
var directory = Path.GetDirectoryName(localPath)!;
Directory.CreateDirectory(directory);
await using var fileStream = new FileStream(localPath, FileMode.Create, FileAccess.Write, FileShare.None);
await response.Content.CopyToAsync(fileStream);
_logger.LogInformation("Cached FMS file locally: {Path} ({Size} bytes)", relativePath, fileStream.Length);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to download from FMS: {Path}", relativePath);
return false;
}
}
private static bool IsAllowedMediaType(string mediaType)
{
return mediaType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)
|| mediaType is "application/pdf"
or "application/octet-stream";
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["CMSMicroservice.WebApi/CMSMicroservice.WebApi.csproj", "CMSMicroservice.WebApi/"]
COPY ["CMSMicroservice.Application/CMSMicroservice.Application.csproj", "CMSMicroservice.Application/"]
@@ -0,0 +1,164 @@
using CMSMicroservice.Application.Common.FileManager;
using Google.Protobuf;
using Google.Protobuf.Reflection;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.Extensions.Logging;
using System.Collections;
using System.Collections.Generic;
namespace CMSMicroservice.WebApi.Interceptors;
/// <summary>
/// gRPC Interceptor — بعد از اجرای هر سرویس، فیلدهای تصویری response را
/// از مسیر نسبی دیسک به base64 data-URI تبدیل می‌کند
/// </summary>
public class ImagePathResolverInterceptor : Interceptor
{
private readonly IFileManager _fileManager;
private readonly ILogger<ImagePathResolverInterceptor> _logger;
// نام فیلدهایی که مسیر تصویر هستند
private static readonly HashSet<string> ImageFieldNames = new(StringComparer.OrdinalIgnoreCase)
{
"image_path",
"thumbnail_path",
"image_thumbnail_path",
"featured_image_path",
"featured_image_thumbnail_path",
"hero_image_path",
"product_thumbnail_path",
"avatar_path",
"avatar_url",
"avatar"
};
public ImagePathResolverInterceptor(IFileManager fileManager, ILogger<ImagePathResolverInterceptor> logger)
{
_fileManager = fileManager;
_logger = logger;
}
// ── Unary call (اکثر gRPC‌ها) ──
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var response = await continuation(request, context);
if (response is IMessage message)
{
ResolveImagePaths(message);
}
return response;
}
// ── Server streaming ──
public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
TRequest request,
IServerStreamWriter<TResponse> responseStream,
ServerCallContext context,
ServerStreamingServerMethod<TRequest, TResponse> continuation)
{
var wrappedStream = new ImageResolvingStreamWriter<TResponse>(responseStream, this);
await continuation(request, wrappedStream, context);
}
/// <summary>
/// بازگشتی: تمام فیلدهای string با نام تصویری را resolve می‌کند
/// شامل فیلدهای تکراری (repeated) و زیر-پیام‌ها (sub-messages)
/// </summary>
internal void ResolveImagePaths(IMessage message)
{
var descriptor = message.Descriptor;
foreach (var field in descriptor.Fields.InFieldNumberOrder())
{
try
{
if (field.FieldType == FieldType.String && ImageFieldNames.Contains(field.Name))
{
// فیلد string ساده
var accessor = field.Accessor;
var value = accessor.GetValue(message) as string;
if (!string.IsNullOrEmpty(value))
{
var resolved = _fileManager.ResolveImageUrl(value);
accessor.SetValue(message, resolved);
}
}
else if (field.FieldType == FieldType.Message)
{
if (field.IsRepeated)
{
// repeated sub-message
var list = field.Accessor.GetValue(message) as System.Collections.IList;
if (list != null)
{
foreach (var item in list)
{
if (item is IMessage subMsg)
ResolveImagePaths(subMsg);
}
}
}
else
{
// فیلد oneof یا فیلد optional message
var subMessage = field.Accessor.GetValue(message) as IMessage;
if (subMessage != null)
{
// Google.Protobuf.WellKnownTypes.StringValue wrapper
if (subMessage is Google.Protobuf.WellKnownTypes.StringValue sv
&& ImageFieldNames.Contains(field.Name))
{
if (!string.IsNullOrEmpty(sv.Value))
{
sv.Value = _fileManager.ResolveImageUrl(sv.Value);
}
}
else
{
ResolveImagePaths(subMessage);
}
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error resolving image path for field {Field}", field.Name);
}
}
}
/// <summary>
/// Wrapper برای Server Streaming — هر پیام قبل از ارسال resolve می‌شود
/// </summary>
private class ImageResolvingStreamWriter<T> : IServerStreamWriter<T> where T : class
{
private readonly IServerStreamWriter<T> _inner;
private readonly ImagePathResolverInterceptor _interceptor;
public ImageResolvingStreamWriter(IServerStreamWriter<T> inner, ImagePathResolverInterceptor interceptor)
{
_inner = inner;
_interceptor = interceptor;
}
public WriteOptions? WriteOptions
{
get => _inner.WriteOptions;
set => _inner.WriteOptions = value;
}
public Task WriteAsync(T message)
{
if (message is IMessage msg)
_interceptor.ResolveImagePaths(msg);
return _inner.WriteAsync(message);
}
}
}
@@ -0,0 +1,73 @@
using CMSMicroservice.Application.Common.Authorization;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Interceptors;
/// <summary>
/// gRPC Interceptor برای بررسی مجوز دسترسی
/// بر اساس [RequiresPermission] attribute روی سرویس‌ها/متدها
/// </summary>
public class PermissionInterceptor : Interceptor
{
private readonly IPermissionService _permissionService;
private readonly ILogger<PermissionInterceptor> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
public PermissionInterceptor(
IPermissionService permissionService,
ILogger<PermissionInterceptor> logger,
IHttpContextAccessor httpContextAccessor)
{
_permissionService = permissionService;
_logger = logger;
_httpContextAccessor = httpContextAccessor;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request, ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
await EnsureHasPermissionAsync(context);
return await continuation(request, context);
}
public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
IAsyncStreamReader<TRequest> requestStream, ServerCallContext context,
ClientStreamingServerMethod<TRequest, TResponse> continuation)
{
await EnsureHasPermissionAsync(context);
return await continuation(requestStream, context);
}
private async Task EnsureHasPermissionAsync(ServerCallContext context)
{
var httpContext = context.GetHttpContext() ?? _httpContextAccessor.HttpContext;
if (httpContext == null) return;
var endpoint = httpContext.GetEndpoint();
if (endpoint == null) return;
var permissionAttributes = endpoint.Metadata.GetOrderedMetadata<RequiresPermissionAttribute>();
if (permissionAttributes == null || permissionAttributes.Count == 0) return;
foreach (var attribute in permissionAttributes)
{
var hasPermission = await _permissionService.HasPermissionAsync(
attribute.Permission, httpContext.RequestAborted);
if (!hasPermission)
{
_logger.LogWarning(
"Permission denied: {Permission} for method {Method}",
attribute.Permission, context.Method);
throw new RpcException(new Status(
StatusCode.PermissionDenied,
$"شما مجوز دسترسی به این عملیات را ندارید ({attribute.Permission})"));
}
}
}
}
+202 -12
View File
@@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using System.Linq;
using Serilog.Core;
using Serilog;
using System.Reflection;
@@ -17,18 +18,19 @@ using CMSMicroservice.WebApi.Common.Behaviours;
using Hangfire;
using Hangfire.SqlServer;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.IO;
var builder = WebApplication.CreateBuilder(args);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
// Configure Kestrel to support both HTTP/1.1 and HTTP/2
builder.WebHost.ConfigureKestrel(options =>
{
builder.WebHost.ConfigureKestrel(options =>
// Enable both HTTP/1.1 and HTTP/2 for all endpoints
options.ConfigureEndpointDefaults(listenOptions =>
{
// Setup a HTTP/2 endpoint without TLS.
options.ListenLocalhost(5000, o => o.Protocols =
HttpProtocols.Http2);
listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
});
}
});
var levelSwitch = new LoggingLevelSwitch();
// Read Seq configuration from appsettings.json
@@ -62,6 +64,8 @@ builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingBehaviour>();
options.Interceptors.Add<PerformanceBehaviour>();
options.Interceptors.Add<CMSMicroservice.WebApi.Interceptors.PermissionInterceptor>();
options.Interceptors.Add<CMSMicroservice.WebApi.Interceptors.ImagePathResolverInterceptor>();
//options.Interceptors.Add<ExceptionHandlingBehaviour>();
options.EnableDetailedErrors = true;
options.MaxReceiveMessageSize = 1000 * 1024 * 1024; // 1 GB
@@ -89,6 +93,13 @@ builder.Services.AddHealthChecks()
// Add Controllers for REST APIs
builder.Services.AddControllers();
// HttpClient for FMS fallback image download
builder.Services.AddHttpClient("FMS", client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("User-Agent", "FourSat-CMS/1.0");
});
#region Configure Cors
builder.Services.AddCors(options =>
@@ -102,15 +113,76 @@ builder.Services.AddCors(options =>
builder.Services.AddGrpcSwagger();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "gRPC transcoding", Version = "v1" });
c.CustomSchemaIds(type=>type.ToString());
// CMS Core Services Documentation
c.SwaggerDoc("cms", new OpenApiInfo
{
Title = "FourSat CMS - Core Services",
Version = "v1",
Description = "Core CMS microservice APIs - Internal business logic and data management",
Contact = new OpenApiContact
{
Name = "FourSat Development Team",
Email = "dev@foursat.com"
}
});
// Admin API Documentation (BFF)
c.SwaggerDoc("admin", new OpenApiInfo
{
Title = "FourSat CMS - Admin BFF",
Version = "v1",
Description = "Admin Backend-for-Frontend API - User Management, Products, Commission, Network, Reports",
Contact = new OpenApiContact
{
Name = "FourSat Development Team",
Email = "dev@foursat.com"
}
});
// Customer API Documentation (BFF)
c.SwaggerDoc("customer", new OpenApiInfo
{
Title = "FourSat CMS - Customer BFF",
Version = "v1",
Description = "Customer Backend-for-Frontend API - Profile, Shop, Commission, Network Statistics",
Contact = new OpenApiContact
{
Name = "FourSat Development Team",
Email = "dev@foursat.com"
}
});
// Unified API Documentation (All endpoints)
c.SwaggerDoc("unified", new OpenApiInfo
{
Title = "FourSat CMS - Unified API",
Version = "v1",
Description = "Complete API Documentation - All Core, Admin & Customer endpoints in one place"
});
c.CustomSchemaIds(type => type.ToString());
// Resolve conflicting actions for Swagger
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
// Include XML documentation for gRPC services
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
if (File.Exists(xmlPath))
{
c.IncludeXmlComments(xmlPath);
}
// Security Definition
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please insert JWT with Bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
Description = "Please insert JWT with Bearer into field (Format: Bearer {token})",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
@@ -125,7 +197,96 @@ builder.Services.AddSwaggerGen(c =>
new string[] { }
}
});
// Group endpoints by functionality
c.TagActionsBy(api =>
{
var serviceName = api.ActionDescriptor.RouteValues?["controller"] ??
GetGrpcServiceName(api.RelativePath);
// Define service categories for better organization
return serviceName switch
{
var name when name.Contains("User") => new[] { "👤 User Management" },
var name when name.Contains("Product") => new[] { "📦 Product Catalog" },
var name when name.Contains("Category") => new[] { "🗂️ Categories" },
var name when name.Contains("Commission") => new[] { "💰 Commission & Earnings" },
var name when name.Contains("Network") => new[] { "🌐 Network & Binary Tree" },
var name when name.Contains("Club") => new[] { "🏆 Club Management" },
var name when name.Contains("Discount") => new[] { "🏷️ Discount Shop" },
var name when name.Contains("Order") => new[] { "🛒 Orders & Shopping" },
var name when name.Contains("Payment") => new[] { "💳 Payments & Transactions" },
var name when name.Contains("Inventory") => new[] { "📋 Inventory Management" },
var name when name.Contains("Health") => new[] { "🔧 System Health" },
var name when name.Contains("Configuration") => new[] { "⚙️ Configuration" },
var name when name.Contains("Admin") => new[] { "👑 Administration" },
_ => new[] { $"📋 {serviceName}" }
};
});
c.DocInclusionPredicate((docName, apiDesc) =>
{
// Get service name from both REST controllers and gRPC services
var controllerName = apiDesc.ActionDescriptor.RouteValues?["controller"] ?? "";
var grpcServiceName = GetGrpcServiceName(apiDesc.RelativePath);
var serviceName = !string.IsNullOrEmpty(controllerName) ? controllerName : grpcServiceName;
return docName switch
{
"cms" => IsCoreService(serviceName),
"admin" => IsAdminService(serviceName),
"customer" => IsCustomerService(serviceName),
"unified" => true, // Show all endpoints
_ => true
};
});
});
// Helper functions for gRPC service name extraction and categorization
static string GetGrpcServiceName(string? relativePath)
{
if (string.IsNullOrEmpty(relativePath)) return "";
var segments = relativePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
return segments.Length > 0 ? segments[0] : "";
}
static bool IsCoreService(string serviceName)
{
var coreServices = new[]
{
"Health", "Configuration", "OtpToken", "Contract", "AppVersion"
};
return coreServices.Any(core => serviceName.Contains(core, StringComparison.OrdinalIgnoreCase));
}
static bool IsAdminService(string serviceName)
{
var adminServices = new[]
{
"Admin", "Role", "UserRole", "ManualPayment", "Inventory",
"Tag", "ProductTag", "ProductGalleries", "ProductImages",
"DiscountOrder", "FactorDetails", "Products", "ProductCategory",
"Category", "DiscountCategory", "DiscountProduct"
};
return adminServices.Any(admin => serviceName.Contains(admin, StringComparison.OrdinalIgnoreCase));
}
static bool IsCustomerService(string serviceName)
{
var customerServices = new[]
{
"User", "UserAddress", "Commission", "NetworkMembership",
"ClubMembership", "UserOrder", "UserWallet", "UserCarts",
"DiscountShoppingCart", "City", "Package", "Transactions",
"UserContract", "UserWalletChangeLog"
};
return customerServices.Any(customer => serviceName.Contains(customer, StringComparison.OrdinalIgnoreCase)) &&
!IsAdminService(serviceName); // Exclude admin services
}
var app = builder.Build();
// Configure the HTTP request pipeline.
@@ -169,6 +330,9 @@ app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
// Enable static files for Swagger custom CSS
app.UseStaticFiles();
// Map Health Check endpoints
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions
@@ -184,6 +348,7 @@ app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); // Configure the H
// Map SignalR Hub for token notifications
app.MapHub<TokenNotificationHub>("/hubs/token-notification");
app.MapHub<TokenNotificationHub>("/hubs/token-relay"); // Alias for FrontOffice backward compatibility
app.ConfigureGrpcEndpoints(Assembly.GetExecutingAssembly(), endpoints =>
{
@@ -194,7 +359,32 @@ app.MapGet("/", () => "Communication with gRPC endpoints must be made through a
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
// CMS Core Services
c.SwaggerEndpoint("/swagger/cms/swagger.json", "🔧 CMS Core Services");
// Admin BFF
c.SwaggerEndpoint("/swagger/admin/swagger.json", "👑 Admin BFF");
// Customer BFF
c.SwaggerEndpoint("/swagger/customer/swagger.json", "👥 Customer BFF");
// Unified API (All endpoints)
c.SwaggerEndpoint("/swagger/unified/swagger.json", "🌐 Unified API (All)");
// UI Customization
c.DocumentTitle = "FourSat CMS API Documentation";
c.RoutePrefix = "swagger"; // Available at /swagger
// Default to CMS core view
c.DefaultModelExpandDepth(2);
c.DefaultModelsExpandDepth(-1);
c.DocExpansion(Swashbuckle.AspNetCore.SwaggerUI.DocExpansion.None);
c.EnableFilter();
c.EnableDeepLinking();
c.EnableValidator();
// Custom CSS
c.InjectStylesheet("/swagger-ui/custom.css");
});
// Configure Hangfire Dashboard
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.Authorization;
using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
@@ -20,11 +21,13 @@ public class AppVersionService : AppVersionContract.AppVersionContractBase
return await _dispatchRequestToCQRS.Handle<GetAppVersionRequest, GetAppVersionQuery, GetAppVersionResponse>(request, context);
}
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
public override async Task<Empty> UpdateAppVersion(UpdateAppVersionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateAppVersionRequest, UpdateAppVersionCommand>(request, context);
}
[RequiresPermission(PermissionNames.SettingsView)]
public override async Task<GetAllAppVersionsResponse> GetAllAppVersions(GetAllAppVersionsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllAppVersionsRequest, GetAllAppVersionsQuery, GetAllAppVersionsResponse>(request, context);
@@ -0,0 +1,116 @@
using CMSMicroservice.Protobuf.Protos.BlogCategory;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.BlogCategoryCQ.Commands.CreateBlogCategory;
using CMSMicroservice.Application.BlogCategoryCQ.Commands.UpdateBlogCategory;
using CMSMicroservice.Application.BlogCategoryCQ.Commands.DeleteBlogCategory;
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetBlogCategory;
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetAllBlogCategories;
using CMSMicroservice.Application.BlogCategoryCQ.Queries.GetActiveBlogCategories;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Mapster;
using MediatR;
using AppModels = CMSMicroservice.Application.Common.Models;
using ProtoMetaData = CMSMicroservice.Protobuf.Protos.MetaData;
namespace CMSMicroservice.WebApi.Services;
public class BlogCategoryService : BlogCategoryContract.BlogCategoryContractBase
{
private readonly ISender _sender;
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public BlogCategoryService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_sender = sender;
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<CreateBlogCategoryResponse> CreateBlogCategory(CreateBlogCategoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateBlogCategoryRequest, CreateBlogCategoryCommand, CreateBlogCategoryResponse>(request, context);
}
public override async Task<Empty> UpdateBlogCategory(UpdateBlogCategoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateBlogCategoryRequest, UpdateBlogCategoryCommand>(request, context);
}
public override async Task<Empty> DeleteBlogCategory(DeleteBlogCategoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteBlogCategoryRequest, DeleteBlogCategoryCommand>(request, context);
}
public override async Task<GetBlogCategoryResponse> GetBlogCategory(GetBlogCategoryRequest request, ServerCallContext context)
{
var query = new GetBlogCategoryQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
return MapCategoryToResponse(result);
}
public override async Task<GetAllBlogCategoriesResponse> GetAllBlogCategories(GetAllBlogCategoriesRequest request, ServerCallContext context)
{
var query = new GetAllBlogCategoriesQuery
{
PageNumber = request.PaginationState?.PageNumber ?? 1,
PageSize = request.PaginationState?.PageSize ?? 20,
SearchTerm = request.SortBy
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllBlogCategoriesResponse
{
MetaData = result.MetaData.Adapt<ProtoMetaData>()
};
foreach (var item in result.Models)
response.Models.Add(MapToListItem(item));
return response;
}
public override async Task<GetActiveBlogCategoriesResponse> GetActiveBlogCategories(GetActiveBlogCategoriesRequest request, ServerCallContext context)
{
var query = new GetActiveBlogCategoriesQuery();
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetActiveBlogCategoriesResponse();
foreach (var item in result)
response.Categories.Add(MapToListItem(item));
return response;
}
// ── Private Mapping Helpers ──
private static GetBlogCategoryResponse MapCategoryToResponse(BlogCategoryDto dto)
{
return new GetBlogCategoryResponse
{
Id = dto.Id,
Title = dto.Title ?? string.Empty,
Slug = dto.Slug ?? string.Empty,
Description = dto.Description,
IconName = dto.IconName,
SortOrder = dto.SortOrder,
IsActive = dto.IsActive,
PostCount = dto.PostCount,
Created = dto.Created != default ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.Created, DateTimeKind.Utc)) : null
};
}
private static BlogCategoryListItem MapToListItem(BlogCategoryDto dto)
{
return new BlogCategoryListItem
{
Id = dto.Id,
Title = dto.Title ?? string.Empty,
Slug = dto.Slug ?? string.Empty,
Description = dto.Description,
IconName = dto.IconName,
SortOrder = dto.SortOrder,
IsActive = dto.IsActive,
PostCount = dto.PostCount
};
}
}
@@ -0,0 +1,72 @@
using System.Linq;
using CMSMicroservice.Protobuf.Protos.BlogPostImage;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.BlogPostImageCQ.Commands.AddBlogPostImage;
using CMSMicroservice.Application.BlogPostImageCQ.Commands.DeleteBlogPostImage;
using CMSMicroservice.Application.BlogPostImageCQ.Commands.ReorderBlogPostImages;
using CMSMicroservice.Application.BlogPostImageCQ.Queries.GetBlogPostImages;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class BlogPostImageService : BlogPostImageContract.BlogPostImageContractBase
{
private readonly ISender _sender;
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public BlogPostImageService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_sender = sender;
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<AddBlogPostImageResponse> AddBlogPostImage(AddBlogPostImageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<AddBlogPostImageRequest, AddBlogPostImageCommand, AddBlogPostImageResponse>(request, context);
}
public override async Task<Empty> DeleteBlogPostImage(DeleteBlogPostImageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteBlogPostImageRequest, DeleteBlogPostImageCommand>(request, context);
}
public override async Task<GetBlogPostImagesResponse> GetBlogPostImages(GetBlogPostImagesRequest request, ServerCallContext context)
{
var query = new GetBlogPostImagesQuery { BlogPostId = request.BlogPostId };
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetBlogPostImagesResponse();
foreach (var item in result)
{
response.Images.Add(new BlogPostImageItem
{
Id = item.Id,
BlogPostId = item.BlogPostId,
ImagePath = item.ImagePath ?? string.Empty,
ThumbnailPath = item.ThumbnailPath ?? string.Empty,
AltText = item.AltText,
Caption = item.Caption,
SortOrder = item.SortOrder
});
}
return response;
}
public override async Task<Empty> ReorderBlogPostImages(ReorderBlogPostImagesRequest request, ServerCallContext context)
{
var command = new ReorderBlogPostImagesCommand
{
Items = request.Items.Select(x => new Application.BlogPostImageCQ.Commands.ReorderBlogPostImages.ImageSortItem
{
Id = x.Id,
SortOrder = x.SortOrder
}).ToList()
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
}
@@ -0,0 +1,231 @@
using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos.BlogPost;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.BlogPostCQ.Commands.CreateBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.UpdateBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.DeleteBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.PublishBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.ArchiveBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Commands.IncrementViewCount;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPost;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetBlogPostBySlug;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetAllBlogPosts;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetPublishedBlogPosts;
using CMSMicroservice.Application.BlogPostCQ.Queries.GetFeaturedBlogPosts;
using CMSMicroservice.Domain.Enums;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Mapster;
using MediatR;
using AppModels = CMSMicroservice.Application.Common.Models;
using ProtoMetaData = CMSMicroservice.Protobuf.Protos.MetaData;
namespace CMSMicroservice.WebApi.Services;
public class BlogPostService : BlogPostContract.BlogPostContractBase
{
private readonly ISender _sender;
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public BlogPostService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_sender = sender;
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<CreateBlogPostResponse> CreateBlogPost(CreateBlogPostRequest request, ServerCallContext context)
{
var command = new CreateBlogPostCommand
{
Title = request.Title,
Slug = request.Slug,
Summary = request.Summary,
HtmlContent = request.HtmlContent,
FeaturedImagePath = request.FeaturedImagePath,
FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath,
CategoryIds = request.CategoryIds.ToList(),
TagIds = request.TagIds.ToList(),
IsFeatured = request.IsFeatured,
SortOrder = request.SortOrder,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new CreateBlogPostResponse { Id = result };
}
public override async Task<Empty> UpdateBlogPost(UpdateBlogPostRequest request, ServerCallContext context)
{
var command = new UpdateBlogPostCommand
{
Id = request.Id,
Title = request.Title,
Slug = request.Slug,
Summary = request.Summary,
HtmlContent = request.HtmlContent,
FeaturedImagePath = request.FeaturedImagePath,
FeaturedImageThumbnailPath = request.FeaturedImageThumbnailPath,
CategoryIds = request.CategoryIds.ToList(),
TagIds = request.TagIds.ToList(),
IsFeatured = request.IsFeatured,
SortOrder = request.SortOrder,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<Empty> DeleteBlogPost(DeleteBlogPostRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteBlogPostRequest, DeleteBlogPostCommand>(request, context);
}
public override async Task<GetBlogPostResponse> GetBlogPost(GetBlogPostRequest request, ServerCallContext context)
{
var query = new GetBlogPostQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
return MapBlogPostDtoToResponse(result);
}
public override async Task<GetBlogPostResponse> GetBlogPostBySlug(GetBlogPostBySlugRequest request, ServerCallContext context)
{
var query = new GetBlogPostBySlugQuery { Slug = request.Slug };
var result = await _sender.Send(query, context.CancellationToken);
return MapBlogPostDtoToResponse(result);
}
public override async Task<GetAllBlogPostsResponse> GetAllBlogPosts(GetAllBlogPostsRequest request, ServerCallContext context)
{
var query = new GetAllBlogPostsQuery
{
PageNumber = request.PaginationState?.PageNumber ?? 1,
PageSize = request.PaginationState?.PageSize ?? 10,
SortBy = request.SortBy,
SearchTerm = request.Filter?.SearchTerm,
Status = request.Filter?.Status.HasValue == true ? (BlogPostStatus?)request.Filter.Status.Value : null,
CategoryId = request.Filter?.CategoryId,
IsFeatured = request.Filter?.IsFeatured
};
var result = await _sender.Send(query, context.CancellationToken);
return MapAllBlogPostsResponse(result);
}
public override async Task<GetAllBlogPostsResponse> GetPublishedBlogPosts(GetPublishedBlogPostsRequest request, ServerCallContext context)
{
var query = new GetPublishedBlogPostsQuery
{
PageNumber = request.PaginationState?.PageNumber ?? 1,
PageSize = request.PaginationState?.PageSize ?? 10,
SearchTerm = request.SearchTerm,
CategoryId = request.CategoryId
};
var result = await _sender.Send(query, context.CancellationToken);
return MapAllBlogPostsResponse(result);
}
public override async Task<GetAllBlogPostsResponse> GetFeaturedBlogPosts(GetFeaturedBlogPostsRequest request, ServerCallContext context)
{
var query = new GetFeaturedBlogPostsQuery { Count = request.Count > 0 ? request.Count : 5 };
var items = await _sender.Send(query, context.CancellationToken);
var response = new GetAllBlogPostsResponse();
foreach (var item in items)
response.Models.Add(MapToListItem(item));
return response;
}
public override async Task<PublishBlogPostResponse> PublishBlogPost(PublishBlogPostRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<PublishBlogPostRequest, PublishBlogPostCommand, PublishBlogPostResponse>(request, context);
}
public override async Task<ArchiveBlogPostResponse> ArchiveBlogPost(ArchiveBlogPostRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ArchiveBlogPostRequest, ArchiveBlogPostCommand, ArchiveBlogPostResponse>(request, context);
}
public override async Task<Empty> IncrementViewCount(IncrementViewCountRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<IncrementViewCountRequest, IncrementViewCountCommand>(request, context);
}
// ── Private Mapping Helpers ──
private static GetBlogPostResponse MapBlogPostDtoToResponse(BlogPostDto dto)
{
var response = new GetBlogPostResponse
{
Id = dto.Id,
Title = dto.Title ?? string.Empty,
Slug = dto.Slug ?? string.Empty,
Summary = dto.Summary,
HtmlContent = dto.HtmlContent ?? string.Empty,
FeaturedImagePath = dto.FeaturedImagePath,
FeaturedImageThumbnailPath = dto.FeaturedImageThumbnailPath,
Status = (int)dto.Status,
StatusName = dto.StatusName ?? string.Empty,
ViewCount = dto.ViewCount,
AuthorUserId = dto.AuthorUserId,
IsFeatured = dto.IsFeatured,
SortOrder = dto.SortOrder,
Created = dto.Created != default ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.Created, DateTimeKind.Utc)) : null,
LastModified = dto.LastModified.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.LastModified.Value, DateTimeKind.Utc)) : null,
PublishedAt = dto.PublishedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(dto.PublishedAt.Value, DateTimeKind.Utc)) : null
};
if (dto.Categories != null)
foreach (var c in dto.Categories)
response.Categories.Add(new BlogPostCategoryInfo { Id = c.Id, Title = c.Title ?? string.Empty, Slug = c.Slug ?? string.Empty });
if (dto.Tags != null)
foreach (var t in dto.Tags)
response.Tags.Add(new BlogPostTagInfo { Id = t.Id, Title = t.Title ?? string.Empty, Name = t.Name ?? string.Empty });
return response;
}
private static GetAllBlogPostsResponse MapAllBlogPostsResponse(GetAllBlogPostsResponseDto dto)
{
var response = new GetAllBlogPostsResponse
{
MetaData = dto.MetaData.Adapt<ProtoMetaData>()
};
foreach (var item in dto.Models)
response.Models.Add(MapToListItem(item));
return response;
}
private static BlogPostListItem MapToListItem(BlogPostListItemDto item)
{
var listItem = new BlogPostListItem
{
Id = item.Id,
Title = item.Title ?? string.Empty,
Slug = item.Slug ?? string.Empty,
Summary = item.Summary,
FeaturedImageThumbnailPath = item.FeaturedImageThumbnailPath,
Status = item.Status,
StatusName = item.StatusName ?? string.Empty,
ViewCount = item.ViewCount,
IsFeatured = item.IsFeatured,
Created = item.Created != default ? Timestamp.FromDateTime(DateTime.SpecifyKind(item.Created, DateTimeKind.Utc)) : null,
PublishedAt = item.PublishedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(item.PublishedAt.Value, DateTimeKind.Utc)) : null
};
if (item.Categories != null)
foreach (var c in item.Categories)
listItem.Categories.Add(new BlogPostCategoryInfo { Id = c.Id, Title = c.Title ?? string.Empty, Slug = c.Slug ?? string.Empty });
return listItem;
}
}
@@ -5,14 +5,19 @@ using CMSMicroservice.Application.CategoryCQ.Commands.UpdateCategory;
using CMSMicroservice.Application.CategoryCQ.Commands.DeleteCategory;
using CMSMicroservice.Application.CategoryCQ.Queries.GetCategory;
using CMSMicroservice.Application.CategoryCQ.Queries.GetAllCategoryByFilter;
using MediatR;
using AppModels = CMSMicroservice.Application.Common.Models;
namespace CMSMicroservice.WebApi.Services;
public class CategoryService : CategoryContract.CategoryContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
public CategoryService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public CategoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
}
public override async Task<CreateNewCategoryResponse> CreateNewCategory(CreateNewCategoryRequest request, ServerCallContext context)
{
@@ -34,4 +39,83 @@ public class CategoryService : CategoryContract.CategoryContractBase
{
return await _dispatchRequestToCQRS.Handle<GetAllCategoryByFilterRequest, GetAllCategoryByFilterQuery, GetAllCategoryByFilterResponse>(request, context);
}
// ============= Customer-specific Methods =============
public override async Task<GetAllCategoriesResponse> GetAllCategories(GetAllCategoriesRequest request, ServerCallContext context)
{
// Reuse existing GetAllCategoryByFilter query but with customer-specific response
return await _dispatchRequestToCQRS.Handle<GetAllCategoriesRequest, GetAllCategoryByFilterQuery, GetAllCategoriesResponse>(request, context);
}
public override async Task<GetAllCategoriesForCustomerResponse> GetAllCategoriesForCustomer(GetAllCategoriesForCustomerRequest request, ServerCallContext context)
{
// Use GetAllCategoryByFilter to get all active categories
var query = new GetAllCategoryByFilterQuery
{
Filter = new CMSMicroservice.Application.CategoryCQ.Queries.GetAllCategoryByFilter.GetAllCategoryByFilterFilter
{
IsActive = true // Only active categories for customers
},
PaginationState = new AppModels.PaginationState
{
PageNumber = request.PageNumber > 0 ? request.PageNumber : 1,
PageSize = request.PageSize > 0 ? request.PageSize : 100 // Default to large page size to get all
},
SortBy = "SortOrder" // Sort by display order
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllCategoriesForCustomerResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = result.MetaData.CurrentPage,
PageSize = result.MetaData.PageSize,
TotalCount = result.MetaData.TotalCount,
TotalPage = result.MetaData.TotalPage,
HasNext = result.MetaData.HasNext,
HasPrevious = result.MetaData.HasPrevious
}
};
foreach (var cat in result.Models)
{
response.Models.Add(new GetAllCategoryFilterResponseModel
{
Id = cat.Id,
Name = cat.Name,
Title = cat.Title,
Description = cat.Description ?? string.Empty,
ImagePath = cat.ImagePath ?? string.Empty,
ParentId = cat.ParentId ?? 0,
IsActive = cat.IsActive,
SortOrder = cat.SortOrder
});
}
return response;
}
public override async Task<GetCategoryByIdForCustomerResponse> GetCategoryByIdForCustomer(GetCategoryByIdForCustomerRequest request, ServerCallContext context)
{
var query = new GetCategoryQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
return new GetCategoryByIdForCustomerResponse
{
Category = new GetAllCategoryFilterResponseModel
{
Id = result.Id,
Name = result.Name,
Title = result.Title,
Description = result.Description ?? string.Empty,
ImagePath = result.ImagePath ?? string.Empty,
ParentId = result.ParentId ?? 0,
IsActive = result.IsActive,
SortOrder = result.SortOrder
}
};
}
}
@@ -1,16 +1,22 @@
using CMSMicroservice.Protobuf.Protos.City;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.CityCQ.Queries.GetAllCitiesByFilter;
using CMSMicroservice.Application.Common.Interfaces;
using Microsoft.EntityFrameworkCore;
using Google.Protobuf.WellKnownTypes;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class CityService : CityContract.CityContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly IApplicationDbContext _context;
public CityService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public CityService(IDispatchRequestToCQRS dispatchRequestToCQRS, IApplicationDbContext context)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_context = context;
}
public override async Task<GetAllCitiesByFilterResponse> GetAllCitiesByFilter(
@@ -22,4 +28,184 @@ public class CityService : CityContract.CityContractBase
GetAllCitiesByFilterQuery,
GetAllCitiesByFilterResponse>(request, context);
}
#region Customer Methods
public override async Task<GetCitiesForCustomerResponse> GetCitiesForCustomer(
GetCitiesForCustomerRequest request, ServerCallContext context)
{
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
var pageSize = request.PageSize > 0 ? request.PageSize : 50;
var query = _context.Cities
.Include(c => c.State)
.Where(c => !c.IsDeleted);
if (request.StateId != null)
query = query.Where(c => c.StateId == request.StateId.Value);
if (!string.IsNullOrWhiteSpace(request.SearchTerm))
query = query.Where(c => c.Name.Contains(request.SearchTerm) || c.Native.Contains(request.SearchTerm));
var totalCount = await query.CountAsync(context.CancellationToken);
var cities = await query
.OrderBy(c => c.Native)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(c => new CityDto
{
Id = c.Id,
ExternalId = c.ExternalId,
Name = c.Name,
Native = c.Native,
Latitude = c.Latitude ?? string.Empty,
Longitude = c.Longitude ?? string.Empty,
StateId = c.StateId,
StateName = c.State != null ? c.State.Name : string.Empty,
StateNative = c.State != null ? c.State.Native : string.Empty
})
.ToListAsync(context.CancellationToken);
return new GetCitiesForCustomerResponse
{
Cities = { cities },
MetaData = new CMSMicroservice.Protobuf.Protos.City.MetaData
{
CurrentPage = pageNumber,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
HasNext = pageNumber * pageSize < totalCount,
HasPrevious = pageNumber > 1
}
};
}
public override async Task<GetCityByIdForCustomerResponse> GetCityByIdForCustomer(
GetCityByIdForCustomerRequest request, ServerCallContext context)
{
var city = await _context.Cities
.Include(c => c.State)
.Where(c => c.Id == request.Id && !c.IsDeleted)
.Select(c => new CityDto
{
Id = c.Id,
ExternalId = c.ExternalId,
Name = c.Name,
Native = c.Native,
Latitude = c.Latitude ?? string.Empty,
Longitude = c.Longitude ?? string.Empty,
StateId = c.StateId,
StateName = c.State != null ? c.State.Name : string.Empty,
StateNative = c.State != null ? c.State.Native : string.Empty
})
.FirstOrDefaultAsync(context.CancellationToken);
if (city == null)
throw new RpcException(new Status(StatusCode.NotFound, "شهر یافت نشد"));
return new GetCityByIdForCustomerResponse { City = city };
}
public override async Task<GetCitiesByStateForCustomerResponse> GetCitiesByStateForCustomer(
GetCitiesByStateForCustomerRequest request, ServerCallContext context)
{
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
var pageSize = request.PageSize > 0 ? request.PageSize : 100;
var query = _context.Cities
.Include(c => c.State)
.Where(c => c.StateId == request.StateId && !c.IsDeleted);
var totalCount = await query.CountAsync(context.CancellationToken);
var cities = await query
.OrderBy(c => c.Native)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(c => new CityDto
{
Id = c.Id,
ExternalId = c.ExternalId,
Name = c.Name,
Native = c.Native,
Latitude = c.Latitude ?? string.Empty,
Longitude = c.Longitude ?? string.Empty,
StateId = c.StateId,
StateName = c.State != null ? c.State.Name : string.Empty,
StateNative = c.State != null ? c.State.Native : string.Empty
})
.ToListAsync(context.CancellationToken);
return new GetCitiesByStateForCustomerResponse
{
Cities = { cities },
MetaData = new CMSMicroservice.Protobuf.Protos.City.MetaData
{
CurrentPage = pageNumber,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
HasNext = pageNumber * pageSize < totalCount,
HasPrevious = pageNumber > 1
}
};
}
// Admin Methods
public override async Task<CreateCityResponse> CreateCity(
CreateCityRequest request, ServerCallContext context)
{
var city = new Domain.Entities.Geography.City
{
ExternalId = request.ExternalId,
Name = request.Name,
Native = request.Native,
Latitude = request.Latitude ?? string.Empty,
Longitude = request.Longitude ?? string.Empty,
StateId = request.StateId
};
_context.Cities.Add(city);
await _context.SaveChangesAsync(context.CancellationToken);
return new CreateCityResponse
{
Id = city.Id,
Message = "شهر با موفقیت ایجاد شد"
};
}
public override async Task<Empty> UpdateCity(
UpdateCityRequest request, ServerCallContext context)
{
var city = await _context.Cities.FindAsync(new object[] { request.Id }, context.CancellationToken);
if (city == null)
throw new RpcException(new Status(StatusCode.NotFound, "شهر یافت نشد"));
city.ExternalId = request.ExternalId;
city.Name = request.Name;
city.Native = request.Native;
if (request.Latitude != null) city.Latitude = request.Latitude;
if (request.Longitude != null) city.Longitude = request.Longitude;
city.StateId = request.StateId;
await _context.SaveChangesAsync(context.CancellationToken);
return new Empty();
}
public override async Task<Empty> DeleteCity(
DeleteCityRequest request, ServerCallContext context)
{
var city = await _context.Cities.FindAsync(new object[] { request.Id }, context.CancellationToken);
if (city == null)
throw new RpcException(new Status(StatusCode.NotFound, "شهر یافت نشد"));
city.IsDeleted = true;
await _context.SaveChangesAsync(context.CancellationToken);
return new Empty();
}
#endregion
}
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.ClubMembershipCQ.Commands.ActivateClubMembership;
@@ -10,16 +11,24 @@ using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubMembershipHist
using CMSMicroservice.Application.ClubMembershipCQ.Queries.GetClubStatistics;
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
using CMSMicroservice.Application.ClubFeatureCQ.Commands.ToggleUserClubFeature;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Services;
public class ClubMembershipService : ClubMembershipContract.ClubMembershipContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ICurrentUserService _currentUserService;
private readonly ILogger<ClubMembershipService> _logger;
public ClubMembershipService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public ClubMembershipService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ICurrentUserService currentUserService,
ILogger<ClubMembershipService> logger)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_currentUserService = currentUserService;
_logger = logger;
}
public override async Task<Empty> ActivateClubMembership(ActivateClubMembershipRequest request, ServerCallContext context)
@@ -39,6 +48,25 @@ public class ClubMembershipService : ClubMembershipContract.ClubMembershipContra
public override async Task<GetClubMembershipResponse> GetClubMembership(GetClubMembershipRequest request, ServerCallContext context)
{
// اگر UserId در request نیست یا صفر است، از JWT بخوان
if (request.UserId <= 0)
{
if (long.TryParse(_currentUserService.UserId, out var tokenUserId) && tokenUserId > 0)
{
_logger.LogInformation("GetClubMembership: Reading UserId from JWT token: {UserId}", tokenUserId);
request = new GetClubMembershipRequest { UserId = tokenUserId };
}
else
{
_logger.LogWarning("GetClubMembership: No valid UserId in request or JWT token");
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
}
}
else
{
_logger.LogInformation("GetClubMembership: Using UserId from request: {UserId}", request.UserId);
}
return await _dispatchRequestToCQRS.Handle<GetClubMembershipRequest, GetClubMembershipQuery, GetClubMembershipResponse>(request, context);
}
@@ -12,6 +12,7 @@ using CMSMicroservice.Application.CommissionCQ.Queries.GetWeeklyCommissionPool;
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserCommissionPayouts;
using CMSMicroservice.Application.CommissionCQ.Queries.GetCommissionPayoutHistory;
using CMSMicroservice.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAllWeeklyPools;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalRequests;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerStatus;
@@ -19,6 +20,7 @@ using CMSMicroservice.Application.CommissionCQ.Queries.GetWorkerExecutionLogs;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWithdrawalReports;
using CMSMicroservice.Application.CommissionCQ.Queries.GetAvailableWeeks;
using CMSMicroservice.Application.CommissionCQ.Queries.GetWeekDefinitions;
using CMSMicroservice.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
namespace CMSMicroservice.WebApi.Services;
@@ -68,6 +70,11 @@ public class CommissionService : CommissionContract.CommissionContractBase
return await _dispatchRequestToCQRS.Handle<GetUserCommissionPayoutsRequest, GetUserCommissionPayoutsQuery, GetUserCommissionPayoutsResponse>(request, context);
}
public override async Task<GetMyCommissionPayoutsResponse> GetMyCommissionPayouts(GetMyCommissionPayoutsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery, GetMyCommissionPayoutsResponse>(request, context);
}
public override async Task<GetCommissionPayoutHistoryResponse> GetCommissionPayoutHistory(GetCommissionPayoutHistoryRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetCommissionPayoutHistoryRequest, GetCommissionPayoutHistoryQuery, GetCommissionPayoutHistoryResponse>(request, context);
@@ -128,4 +135,9 @@ public class CommissionService : CommissionContract.CommissionContractBase
{
return await _dispatchRequestToCQRS.Handle<GetWithdrawalReportsRequest, GetWithdrawalReportsQuery, GetWithdrawalReportsResponse>(request, context);
}
public override async Task<GetMyWeeklyBalancesResponse> GetMyWeeklyBalances(GetMyWeeklyBalancesRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery, GetMyWeeklyBalancesResponse>(request, context);
}
}
@@ -1,7 +1,11 @@
using CMSMicroservice.Application.ClubFeatureCQ.Queries.GetUserClubFeatures;
using CMSMicroservice.Application.Common.Authorization;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Common;
using CMSMicroservice.Protobuf.Protos.Configuration;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.WebApi.Services;
@@ -12,10 +16,17 @@ namespace CMSMicroservice.WebApi.Services;
public class ConfigurationService : ConfigurationContract.ConfigurationContractBase
{
private readonly ILogger<ConfigurationService> _logger;
private readonly ICurrentUserService _currentUserService;
private readonly IMediator _mediator;
public ConfigurationService(ILogger<ConfigurationService> logger)
public ConfigurationService(
ILogger<ConfigurationService> logger,
ICurrentUserService currentUserService,
IMediator mediator)
{
_logger = logger;
_currentUserService = currentUserService;
_mediator = mediator;
}
/// <summary>
@@ -41,9 +52,65 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
return Task.FromResult(response);
}
/// <summary>
/// دریافت تنظیمات باشگاه مشتریان
/// </summary>
public override Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
{
var response = new GetClubConfigurationResponse
{
ActivationFee = SystemConstants.ClubActivationFee,
MembershipGiftValue = SystemConstants.ClubMembershipGiftValue
};
_logger.LogDebug("Club configuration requested: ActivationFee={ActivationFee}, GiftValue={GiftValue}",
response.ActivationFee, response.MembershipGiftValue);
return Task.FromResult(response);
}
/// <summary>
/// دریافت ویژگی‌های باشگاه مشتریان برای کاربر جاری
/// </summary>
public override async Task<GetClubFeaturesResponse> GetClubFeatures(Empty request, ServerCallContext context)
{
// دریافت UserId از JWT
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
{
_logger.LogWarning("GetClubFeatures called without valid user authentication");
return new GetClubFeaturesResponse(); // لیست خالی برای کاربران غیر احراز هویت شده
}
// فراخوانی GetUserClubFeatures از طریق MediatR (CQRS داخلی)
var query = new GetUserClubFeaturesQuery { UserId = userId };
var userFeatures = await _mediator.Send(query, context.CancellationToken);
// تبدیل به فرمت GetClubFeaturesResponse
var response = new GetClubFeaturesResponse();
foreach (var feature in userFeatures)
{
response.Features.Add(new ClubFeatureModel
{
Id = feature.ClubFeatureId,
Title = feature.FeatureTitle ?? string.Empty,
Description = feature.FeatureDescription ?? string.Empty,
IsEnabled = feature.IsActive,
DisplayOrder = feature.SortOrder,
GrantedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(feature.GrantedAt, DateTimeKind.Utc)),
CreatedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(feature.CreatedAt, DateTimeKind.Utc)),
Notes = feature.Notes ?? string.Empty
});
}
_logger.LogDebug("Club features requested for user {UserId}: {Count} features returned", userId, response.Features.Count);
return response;
}
/// <summary>
/// دریافت تمام تنظیمات
/// </summary>
[RequiresPermission(PermissionNames.SettingsView)]
public override Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
{
var response = new GetAllConfigurationsResponse();
@@ -80,16 +147,18 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
/// <summary>
/// سایر عملیات‌ها که فعلاً پیاده‌سازی نشده‌اند (چون از constant استفاده می‌کنیم)
/// </summary>
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
public override Task<Empty> CreateOrUpdateConfiguration(CreateOrUpdateConfigurationRequest request, ServerCallContext context)
{
_logger.LogWarning("CreateOrUpdateConfiguration called but SystemConstants are read-only");
throw new RpcException(new Status(StatusCode.Unimplemented, "تنظیمات سیستم فقط خواندنی هستند"));
_logger.LogWarning("CreateOrUpdateConfiguration called but SystemConstants are read-only. Key: {Key}", request.Key);
throw new RpcException(new Status(StatusCode.FailedPrecondition, "تنظیمات سیستم فقط خواندنی هستند. تغییر از طریق کد انجام می‌شود"));
}
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
public override Task<Empty> DeactivateConfiguration(DeactivateConfigurationRequest request, ServerCallContext context)
{
_logger.LogWarning("DeactivateConfiguration called but SystemConstants are read-only");
throw new RpcException(new Status(StatusCode.Unimplemented, "تنظیمات سیستم فقط خواندنی هستند"));
_logger.LogWarning("DeactivateConfiguration called but SystemConstants are read-only. Key: {Key}", request.Key);
throw new RpcException(new Status(StatusCode.FailedPrecondition, "تنظیمات سیستم فقط خواندنی هستند. غیرفعال‌سازی از طریق کد انجام می‌شود"));
}
public override Task<GetConfigurationHistoryResponse> GetConfigurationHistory(GetConfigurationHistoryRequest request, ServerCallContext context)
@@ -1,25 +1,66 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.PlaceOrder;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CompleteOrderPayment;
using CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateOrderStatus;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetOrderById;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetUserOrders;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetAllDiscountOrders;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountSalesReport;
using DomainEnums = CMSMicroservice.Domain.Enums;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using Mapster;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly ICurrentUserService _currentUserService;
public DiscountOrderService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public DiscountOrderService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
ICurrentUserService currentUserService)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_currentUserService = currentUserService;
}
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var uid) && uid > 0)
return uid;
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
}
public override async Task<PlaceOrderResponse> PlaceOrder(PlaceOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<PlaceOrderRequest, PlaceOrderCommand, PlaceOrderResponse>(request, context);
var command = new PlaceOrderCommand
{
UserId = GetCurrentUserId(),
UserAddressId = request.UserAddressId,
DiscountBalanceToUse = request.DiscountBalanceToUse
};
var result = await _sender.Send(command);
var response = new PlaceOrderResponse
{
Success = result.Success,
Message = result.Message ?? string.Empty,
OrderId = result.OrderId ?? 0,
GatewayAmount = result.GatewayAmountRequired,
};
if (!string.IsNullOrEmpty(result.PaymentUrl))
response.PaymentUrl = result.PaymentUrl;
return response;
}
public override async Task<CompleteOrderPaymentResponse> CompleteOrderPayment(CompleteOrderPaymentRequest request, ServerCallContext context)
@@ -34,11 +75,121 @@ public class DiscountOrderService : DiscountOrderContract.DiscountOrderContractB
public override async Task<GetOrderByIdResponse> GetOrderById(GetOrderByIdRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetOrderByIdRequest, GetOrderByIdQuery, GetOrderByIdResponse>(request, context);
var query = new GetOrderByIdQuery
{
OrderId = request.OrderId,
UserId = GetCurrentUserId()
};
var result = await _sender.Send(query);
if (result == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var response = new GetOrderByIdResponse
{
Id = result.Id,
UserId = result.UserId,
TotalPrice = result.TotalAmount,
DiscountBalanceUsed = result.DiscountBalanceUsed,
GatewayAmount = result.GatewayAmountPaid,
PaymentCompleted = result.PaymentStatus == DomainEnums.PaymentStatus.Success,
PaymentStatus = MapPaymentStatus(result.PaymentStatus),
DeliveryStatus = MapDeliveryStatus(result.DeliveryStatus),
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(result.Created, DateTimeKind.Utc)),
};
if (result.TrackingCode != null) response.TrackingCode = result.TrackingCode;
if (result.DeliveryDescription != null) response.Notes = result.DeliveryDescription;
if (result.Address != null)
{
response.Address = new AddressInfo
{
Title = result.Address.Title ?? "",
Address = result.Address.Address ?? "",
PostalCode = result.Address.PostalCode ?? ""
};
}
foreach (var item in result.Items)
{
response.Items.Add(new CMSMicroservice.Protobuf.Protos.DiscountOrder.OrderItemDto
{
ProductId = item.ProductId,
ProductTitle = item.ProductTitle ?? "",
UnitPrice = item.UnitPrice,
MaxDiscountPercent = item.DiscountPercentUsed,
Count = item.Count,
TotalPrice = item.UnitPrice * item.Count,
DiscountAmount = item.DiscountAmount,
FinalPrice = item.FinalPrice,
ImagePath = item.ImagePath ?? "",
ThumbnailPath = item.ThumbnailPath ?? ""
});
}
return response;
}
public override async Task<GetUserOrdersResponse> GetUserOrders(GetUserOrdersRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserOrdersRequest, GetUserOrdersQuery, GetUserOrdersResponse>(request, context);
var query = new GetUserOrdersQuery
{
UserId = GetCurrentUserId()
};
var result = await _sender.Send(query);
var response = new GetUserOrdersResponse
{
MetaData = result.MetaData.Adapt<CMSMicroservice.Protobuf.Protos.MetaData>()
};
foreach (var o in result.Models)
{
var summary = new CMSMicroservice.Protobuf.Protos.DiscountOrder.OrderSummaryDto
{
Id = o.Id,
TotalPrice = o.TotalAmount,
DiscountBalanceUsed = o.DiscountBalanceUsed,
GatewayAmount = o.GatewayAmountPaid,
PaymentCompleted = o.PaymentStatus == DomainEnums.PaymentStatus.Success,
PaymentStatus = MapPaymentStatus(o.PaymentStatus),
DeliveryStatus = MapDeliveryStatus(o.DeliveryStatus),
ItemsCount = o.ItemsCount,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(o.Created, DateTimeKind.Utc)),
};
if (o.TrackingCode != null) summary.TrackingCode = o.TrackingCode;
response.Models.Add(summary);
}
return response;
}
public override async Task<GetAllDiscountOrdersResponse> GetAllDiscountOrders(GetAllDiscountOrdersRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllDiscountOrdersRequest, GetAllDiscountOrdersQuery, GetAllDiscountOrdersResponse>(request, context);
}
public override async Task<GetDiscountSalesReportResponse> GetDiscountSalesReport(GetDiscountSalesReportRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetDiscountSalesReportRequest, GetDiscountSalesReportQuery, GetDiscountSalesReportResponse>(request, context);
}
private static CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus MapPaymentStatus(DomainEnums.PaymentStatus status) => status switch
{
DomainEnums.PaymentStatus.Success => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentCompleted,
DomainEnums.PaymentStatus.Reject => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentFailed,
DomainEnums.PaymentStatus.Pending => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentPending,
_ => CMSMicroservice.Protobuf.Protos.DiscountOrder.PaymentStatus.PaymentPending
};
private static DeliveryStatus MapDeliveryStatus(DomainEnums.DeliveryStatus status) => status switch
{
DomainEnums.DeliveryStatus.None => DeliveryStatus.DeliveryPending,
DomainEnums.DeliveryStatus.Pending => DeliveryStatus.DeliveryProcessing,
DomainEnums.DeliveryStatus.InTransit => DeliveryStatus.DeliveryShipped,
DomainEnums.DeliveryStatus.Delivered => DeliveryStatus.DeliveryDelivered,
DomainEnums.DeliveryStatus.Returned => DeliveryStatus.DeliveryCancelled,
DomainEnums.DeliveryStatus.Cancelled => DeliveryStatus.DeliveryCancelled,
_ => DeliveryStatus.DeliveryPending
};
}
@@ -1,30 +1,86 @@
using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.DiscountShopCQ.Commands.CreateDiscountProduct;
using CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateDiscountProduct;
using CMSMicroservice.Application.DiscountShopCQ.Commands.DeleteDiscountProduct;
using CMSMicroservice.Application.DiscountShopCQ.Commands.AddDiscountProductImage;
using CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateDiscountProductImage;
using CMSMicroservice.Application.DiscountShopCQ.Commands.DeleteDiscountProductImage;
using CMSMicroservice.Application.DiscountShopCQ.Commands.ReorderDiscountProductImages;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProductById;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProducts;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetDiscountProductImages;
using MediatR;
using Mapster;
namespace CMSMicroservice.WebApi.Services;
public class DiscountProductService : DiscountProductContract.DiscountProductContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
public DiscountProductService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public DiscountProductService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
}
public override async Task<CreateDiscountProductResponse> CreateDiscountProduct(CreateDiscountProductRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateDiscountProductRequest, CreateDiscountProductCommand, CreateDiscountProductResponse>(request, context);
var command = new CreateDiscountProductCommand
{
Title = request.Title,
ShortInfomation = request.ShortInfomation,
FullInformation = request.FullInformation,
Price = request.Price,
MaxDiscountPercent = request.MaxDiscountPercent,
ImagePath = request.ImagePath,
ThumbnailPath = request.ThumbnailPath,
SortOrder = request.SortOrder,
IsActive = request.IsActive,
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
// Map binary image data
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName,
ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(),
ThumbnailFileMime = request.ThumbnailFile?.Mime,
ThumbnailFileName = request.ThumbnailFile?.FileName
};
var productId = await _sender.Send(command, context.CancellationToken);
return new CreateDiscountProductResponse { ProductId = productId };
}
public override async Task<Empty> UpdateDiscountProduct(UpdateDiscountProductRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateDiscountProductRequest, UpdateDiscountProductCommand>(request, context);
var command = new UpdateDiscountProductCommand
{
ProductId = request.ProductId,
Title = request.Title,
ShortInfomation = request.ShortInfomation,
FullInformation = request.FullInformation,
Price = request.Price,
MaxDiscountPercent = request.MaxDiscountPercent,
ImagePath = request.ImagePath,
ThumbnailPath = request.ThumbnailPath,
SortOrder = request.SortOrder,
IsActive = request.IsActive,
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
// Map binary image data
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName,
ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(),
ThumbnailFileMime = request.ThumbnailFile?.Mime,
ThumbnailFileName = request.ThumbnailFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<Empty> DeleteDiscountProduct(DeleteDiscountProductRequest request, ServerCallContext context)
@@ -41,4 +97,35 @@ public class DiscountProductService : DiscountProductContract.DiscountProductCon
{
return await _dispatchRequestToCQRS.Handle<GetDiscountProductsRequest, GetDiscountProductsQuery, GetDiscountProductsResponse>(request, context);
}
// Product Image Gallery Operations
public override async Task<AddDiscountProductImageResponse> AddDiscountProductImage(AddDiscountProductImageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<AddDiscountProductImageRequest, AddDiscountProductImageCommand, AddDiscountProductImageResponse>(request, context);
}
public override async Task<Empty> UpdateDiscountProductImage(UpdateDiscountProductImageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateDiscountProductImageRequest, UpdateDiscountProductImageCommand>(request, context);
}
public override async Task<Empty> DeleteDiscountProductImage(DeleteDiscountProductImageRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteDiscountProductImageRequest, DeleteDiscountProductImageCommand>(request, context);
}
public override async Task<Empty> ReorderDiscountProductImages(ReorderDiscountProductImagesRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ReorderDiscountProductImagesRequest, ReorderDiscountProductImagesCommand>(request, context);
}
public override async Task<GetDiscountProductImagesResponse> GetDiscountProductImages(GetDiscountProductImagesRequest request, ServerCallContext context)
{
var query = request.Adapt<GetDiscountProductImagesQuery>();
var images = await _sender.Send(query, context.CancellationToken);
var response = new GetDiscountProductImagesResponse();
response.Images.AddRange(images.Select(i => i.Adapt<CMSMicroservice.Protobuf.Protos.DiscountProduct.DiscountProductImageDto>()));
return response;
}
}
@@ -1,44 +1,111 @@
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCart;
using CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCart;
using CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCartItemCount;
using CMSMicroservice.Application.DiscountShopCQ.Commands.ClearCart;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetUserCart;
using Grpc.Core;
using Mapster;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class DiscountShoppingCartService : DiscountShoppingCartContract.DiscountShoppingCartContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly ICurrentUserService _currentUserService;
public DiscountShoppingCartService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public DiscountShoppingCartService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
ICurrentUserService currentUserService)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_currentUserService = currentUserService;
}
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var uid) && uid > 0)
return uid;
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
}
public override async Task<AddToCartResponse> AddToCart(AddToCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<AddToCartRequest, AddToCartCommand, AddToCartResponse>(request, context);
var command = new AddToCartCommand
{
UserId = GetCurrentUserId(),
ProductId = request.ProductId,
Count = request.Count
};
var result = await _sender.Send(command, context.CancellationToken);
return result.Adapt<AddToCartResponse>();
}
public override async Task<RemoveFromCartResponse> RemoveFromCart(RemoveFromCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<RemoveFromCartRequest, RemoveFromCartCommand, RemoveFromCartResponse>(request, context);
var command = new RemoveFromCartCommand
{
UserId = GetCurrentUserId(),
ProductId = request.ProductId
};
var result = await _sender.Send(command, context.CancellationToken);
return result.Adapt<RemoveFromCartResponse>();
}
public override async Task<UpdateCartItemCountResponse> UpdateCartItemCount(UpdateCartItemCountRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateCartItemCountRequest, UpdateCartItemCountCommand, UpdateCartItemCountResponse>(request, context);
var command = new UpdateCartItemCountCommand
{
UserId = GetCurrentUserId(),
ProductId = request.ProductId,
NewCount = request.NewCount
};
var result = await _sender.Send(command, context.CancellationToken);
return result.Adapt<UpdateCartItemCountResponse>();
}
public override async Task<GetUserCartResponse> GetUserCart(GetUserCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserCartRequest, GetUserCartQuery, GetUserCartResponse>(request, context);
var query = new GetUserCartQuery { UserId = GetCurrentUserId() };
var cart = await _sender.Send(query, context.CancellationToken);
var response = new GetUserCartResponse
{
TotalPrice = cart.TotalAmount,
TotalDiscountAmount = cart.MaxDiscountAmount,
FinalPrice = cart.MinPayableAmount
};
foreach (var item in cart.Items)
{
response.Items.Add(new CMSMicroservice.Protobuf.Protos.DiscountShoppingCart.CartItemDto
{
ProductId = item.ProductId,
ProductTitle = item.ProductTitle ?? string.Empty,
ProductImagePath = item.ProductImagePath ?? string.Empty,
UnitPrice = item.UnitPrice,
MaxDiscountPercent = item.MaxDiscountPercent,
Count = item.Count,
TotalPrice = item.SubTotal,
DiscountAmount = item.MaxDiscountAmount,
FinalPrice = item.MinPayable,
ProductRemainingCount = item.RemainingStock
});
}
return response;
}
public override async Task<Empty> ClearCart(ClearCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ClearCartRequest, ClearCartCommand>(request, context);
var command = new ClearCartCommand { UserId = GetCurrentUserId() };
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
}
@@ -0,0 +1,48 @@
using CMSMicroservice.Protobuf.Protos.Health;
using CMSMicroservice.Application.HealthCQ.Queries.GetSystemHealth;
using MediatR;
using Mapster;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class HealthService : HealthContract.HealthContractBase
{
private readonly IMediator _mediator;
public HealthService(IMediator mediator)
{
_mediator = mediator;
}
public override async Task<GetSystemHealthResponse> GetSystemHealth(Empty request, ServerCallContext context)
{
var query = new GetSystemHealthQuery();
var result = await _mediator.Send(query, context.CancellationToken);
return result.Adapt<GetSystemHealthResponse>();
}
public override async Task<GetServiceHealthResponse> GetServiceHealth(GetServiceHealthRequest request, ServerCallContext context)
{
// For now, just return system health filtered by service name
var systemHealthQuery = new GetSystemHealthQuery();
var systemHealth = await _mediator.Send(systemHealthQuery, context.CancellationToken);
var service = systemHealth.Services.FirstOrDefault(s =>
s.ServiceName.Equals(request.ServiceName, StringComparison.OrdinalIgnoreCase));
if (service == null)
{
throw new ArgumentException($"Service '{request.ServiceName}' not found");
}
return new GetServiceHealthResponse
{
Service = service.Adapt<ServiceHealthModel>(),
CheckedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.UtcNow)
};
}
}
@@ -0,0 +1,42 @@
using CMSMicroservice.Application.Common.FileManager;
using CMSMicroservice.Protobuf.Protos.ImageResolver;
using Grpc.Core;
namespace CMSMicroservice.WebApi.Services;
/// <summary>
/// سرویس اختصاصی resolve تصاویر — مسیر نسبی را به base64 data-URI تبدیل می‌کند.
/// FrontOffice از طریق این سرویس تمام تصاویر دینامیک را دریافت می‌کند.
/// </summary>
public class ImageResolverService : ImageResolverContract.ImageResolverContractBase
{
private readonly IFileManager _fileManager;
public ImageResolverService(IFileManager fileManager)
{
_fileManager = fileManager;
}
public override Task<ResolveImagesResponse> ResolveImages(ResolveImagesRequest request, ServerCallContext context)
{
var response = new ResolveImagesResponse();
foreach (var path in request.Paths)
{
var dataUri = string.Empty;
if (!string.IsNullOrWhiteSpace(path))
{
dataUri = _fileManager.ResolveImageUrl(path);
}
response.Images.Add(new ResolvedImage
{
OriginalPath = path ?? string.Empty,
DataUri = dataUri ?? string.Empty
});
}
return Task.FromResult(response);
}
}
@@ -0,0 +1,472 @@
using CMSMicroservice.Protobuf.Protos.Inventory;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.WarehouseCQ.Commands.CreateWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Commands.UpdateWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Commands.DeleteWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Commands.SetDefaultWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Queries.GetWarehouse;
using CMSMicroservice.Application.WarehouseCQ.Queries.GetAllWarehouses;
using CMSMicroservice.Application.InventoryItemCQ.Commands.UpdateInventoryItem;
using CMSMicroservice.Application.InventoryItemCQ.Commands.IncreaseInventory;
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReduceInventory;
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReserveInventory;
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReleaseReservedInventory;
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetInventoryItem;
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetInventoryByProduct;
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetAllInventoryItems;
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetLowStockItems;
using CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
using CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovements;
using CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovementsByInventoryItem;
using CMSMicroservice.Application.Common.Interfaces;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class InventoryService : InventoryContract.InventoryContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly IMediator _mediator;
private readonly IApplicationDbContext _context;
public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, IMediator mediator, IApplicationDbContext context)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_mediator = mediator;
_context = context;
}
// ========== Warehouse Management ==========
public override async Task<CreateWarehouseResponse> CreateWarehouse(CreateWarehouseRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateWarehouseRequest, CreateWarehouseCommand, CreateWarehouseResponse>(request, context);
}
public override async Task<Empty> UpdateWarehouse(UpdateWarehouseRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateWarehouseRequest, UpdateWarehouseCommand>(request, context);
}
public override async Task<Empty> DeleteWarehouse(DeleteWarehouseRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteWarehouseRequest, DeleteWarehouseCommand>(request, context);
}
public override async Task<GetWarehouseResponse> GetWarehouse(GetWarehouseRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetWarehouseRequest, GetWarehouseQuery, GetWarehouseResponse>(request, context);
}
public override async Task<GetAllWarehousesResponse> GetAllWarehouses(GetAllWarehousesRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllWarehousesRequest, GetAllWarehousesQuery, GetAllWarehousesResponse>(request, context);
}
public override async Task<Empty> SetDefaultWarehouse(SetDefaultWarehouseRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<SetDefaultWarehouseRequest, SetDefaultWarehouseCommand>(request, context);
}
// ========== Inventory Item Management ==========
public override async Task<GetInventoryItemResponse> GetInventoryItem(GetInventoryItemRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetInventoryItemRequest, GetInventoryItemQuery, GetInventoryItemResponse>(request, context);
}
public override async Task<GetInventoryByProductResponse> GetInventoryByProduct(GetInventoryByProductRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetInventoryByProductRequest, GetInventoryByProductQuery, GetInventoryByProductResponse>(request, context);
}
public override async Task<GetAllInventoryItemsResponse> GetAllInventoryItems(GetAllInventoryItemsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllInventoryItemsRequest, GetAllInventoryItemsQuery, GetAllInventoryItemsResponse>(request, context);
}
public override async Task<GetLowStockItemsResponse> GetLowStockItems(GetLowStockItemsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetLowStockItemsRequest, GetLowStockItemsQuery, GetLowStockItemsResponse>(request, context);
}
public override async Task<Empty> UpdateInventorySettings(UpdateInventorySettingsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateInventorySettingsRequest, UpdateInventoryItemCommand>(request, context);
}
// ========== Stock Operations ==========
public override async Task<AddStockResponse> AddStock(AddStockRequest request, ServerCallContext context)
{
// Lookup InventoryItem by ProductId + ProductType
var inventoryItem = await _mediator.Send(
new GetInventoryByProductQuery
{
ProductId = request.ProductId,
ProductType = (Domain.Enums.ProductType)request.ProductType
},
context.CancellationToken);
if (inventoryItem == null)
{
throw new RpcException(new Status(StatusCode.NotFound,
$"Inventory item not found for ProductId={request.ProductId}, ProductType={request.ProductType}"));
}
// Execute IncreaseInventoryCommand
var response = await _mediator.Send(
new IncreaseInventoryCommand
{
Id = inventoryItem.Id,
Quantity = request.Quantity,
ReferenceNumber = request.ReferenceNumber
},
context.CancellationToken);
return new AddStockResponse
{
InventoryItemId = inventoryItem.Id,
NewQuantity = response.NewQuantity
};
}
public override async Task<AdjustStockResponse> AdjustStock(AdjustStockRequest request, ServerCallContext context)
{
// Lookup InventoryItem
var inventoryItem = await _mediator.Send(
new GetInventoryByProductQuery
{
ProductId = request.ProductId,
ProductType = (Domain.Enums.ProductType)request.ProductType
},
context.CancellationToken);
if (inventoryItem == null)
{
throw new RpcException(new Status(StatusCode.NotFound,
$"Inventory item not found for ProductId={request.ProductId}, ProductType={request.ProductType}"));
}
// Determine if increase or decrease
var difference = request.NewQuantity - inventoryItem.Quantity;
if (difference > 0)
{
await _mediator.Send(
new IncreaseInventoryCommand
{
Id = inventoryItem.Id,
Quantity = difference,
ReferenceNumber = request.ReferenceNumber
},
context.CancellationToken);
return new AdjustStockResponse
{
PreviousQuantity = inventoryItem.Quantity,
NewQuantity = request.NewQuantity,
Difference = difference
};
}
else if (difference < 0)
{
await _mediator.Send(
new ReduceInventoryCommand
{
Id = inventoryItem.Id,
Quantity = Math.Abs(difference),
FromReserved = false,
ReferenceNumber = request.ReferenceNumber
},
context.CancellationToken);
return new AdjustStockResponse
{
PreviousQuantity = inventoryItem.Quantity,
NewQuantity = request.NewQuantity,
Difference = difference
};
}
else
{
return new AdjustStockResponse
{
PreviousQuantity = inventoryItem.Quantity,
NewQuantity = inventoryItem.Quantity,
Difference = 0
};
}
}
public override async Task<ReserveStockResponse> ReserveStock(ReserveStockRequest request, ServerCallContext context)
{
var inventoryItem = await _mediator.Send(
new GetInventoryByProductQuery
{
ProductId = request.ProductId,
ProductType = (Domain.Enums.ProductType)request.ProductType
},
context.CancellationToken);
if (inventoryItem == null)
return new ReserveStockResponse { Success = false, Message = "آیتم موجودی یافت نشد", AvailableQuantity = 0 };
var available = inventoryItem.Quantity - inventoryItem.ReservedQuantity;
if (available < request.Quantity)
return new ReserveStockResponse { Success = false, Message = $"موجودی کافی نیست. موجود: {available}", AvailableQuantity = available };
await _mediator.Send(
new ReserveInventoryCommand
{
Id = inventoryItem.Id,
Quantity = request.Quantity,
ReferenceNumber = request.OrderId != null ? $"ORDER-{request.OrderId.Value}" : $"RESERVE-{DateTime.UtcNow.Ticks}"
},
context.CancellationToken);
return new ReserveStockResponse { Success = true, Message = "رزرو با موفقیت انجام شد", AvailableQuantity = available - request.Quantity };
}
public override async Task<Empty> ReleaseReservation(ReleaseReservationRequest request, ServerCallContext context)
{
var inventoryItem = await _mediator.Send(
new GetInventoryByProductQuery
{
ProductId = request.ProductId,
ProductType = (Domain.Enums.ProductType)request.ProductType
},
context.CancellationToken);
if (inventoryItem == null)
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
await _mediator.Send(
new ReleaseReservedInventoryCommand
{
Id = inventoryItem.Id,
Quantity = request.Quantity,
ReferenceNumber = request.OrderId != null ? $"RELEASE-ORDER-{request.OrderId.Value}" : $"RELEASE-{DateTime.UtcNow.Ticks}"
},
context.CancellationToken);
return new Empty();
}
public override async Task<Empty> ConfirmSale(ConfirmSaleRequest request, ServerCallContext context)
{
var inventoryItem = await _mediator.Send(
new GetInventoryByProductQuery
{
ProductId = request.ProductId,
ProductType = (Domain.Enums.ProductType)request.ProductType
},
context.CancellationToken);
if (inventoryItem == null)
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
await _mediator.Send(
new ReduceInventoryCommand
{
Id = inventoryItem.Id,
Quantity = request.Quantity,
FromReserved = request.FromReservation,
ReferenceNumber = request.OrderId != null ? $"SALE-ORDER-{request.OrderId.Value}" : $"SALE-{DateTime.UtcNow.Ticks}"
},
context.CancellationToken);
return new Empty();
}
public override async Task<ProcessReturnResponse> ProcessReturn(ProcessReturnRequest request, ServerCallContext context)
{
var inventoryItem = await _mediator.Send(
new GetInventoryByProductQuery
{
ProductId = request.ProductId,
ProductType = (Domain.Enums.ProductType)request.ProductType
},
context.CancellationToken);
if (inventoryItem == null)
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
var result = await _mediator.Send(
new IncreaseInventoryCommand
{
Id = inventoryItem.Id,
Quantity = request.Quantity,
ReferenceNumber = request.OrderId != null ? $"RETURN-ORDER-{request.OrderId.Value}" : $"RETURN-{DateTime.UtcNow.Ticks}"
},
context.CancellationToken);
return new ProcessReturnResponse { NewQuantity = result.NewQuantity };
}
public override async Task<Empty> RecordLoss(RecordLossRequest request, ServerCallContext context)
{
// Lookup InventoryItem by ProductId + ProductType
var inventoryItem = await _mediator.Send(
new GetInventoryByProductQuery
{
ProductId = request.ProductId,
ProductType = (Domain.Enums.ProductType)request.ProductType
},
context.CancellationToken);
if (inventoryItem == null)
{
throw new RpcException(new Status(StatusCode.NotFound,
$"Inventory item not found for ProductId={request.ProductId}, ProductType={request.ProductType}"));
}
// Execute ReduceInventoryCommand to record the loss
await _mediator.Send(
new ReduceInventoryCommand
{
Id = inventoryItem.Id,
Quantity = request.Quantity,
FromReserved = false,
ReferenceNumber = request.ReferenceNumber ?? $"LOSS-{DateTime.UtcNow:yyyyMMddHHmmss}"
},
context.CancellationToken);
return new Empty();
}
// ========== Bulk Operations ==========
public override async Task<BulkAddStockResponse> BulkAddStock(BulkAddStockRequest request, ServerCallContext context)
{
var response = new BulkAddStockResponse();
foreach (var item in request.Items)
{
try
{
var inventoryItem = await _mediator.Send(
new GetInventoryByProductQuery
{
ProductId = item.ProductId,
ProductType = (Domain.Enums.ProductType)item.ProductType
},
context.CancellationToken);
if (inventoryItem == null)
{
response.FailedCount++;
response.Errors.Add($"ProductId={item.ProductId}: آیتم موجودی یافت نشد");
continue;
}
await _mediator.Send(
new IncreaseInventoryCommand
{
Id = inventoryItem.Id,
Quantity = item.Quantity,
ReferenceNumber = request.ReferenceNumber ?? $"BULK-{DateTime.UtcNow.Ticks}"
},
context.CancellationToken);
response.SuccessCount++;
}
catch (Exception ex)
{
response.FailedCount++;
response.Errors.Add($"ProductId={item.ProductId}: {ex.Message}");
}
}
return response;
}
// ========== Stock Movements ==========
public override async Task<GetStockMovementsResponse> GetStockMovements(GetStockMovementsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetStockMovementsRequest, GetStockMovementsQuery, GetStockMovementsResponse>(request, context);
}
public override async Task<GetStockMovementsByInventoryItemResponse> GetStockMovementsByInventoryItem(GetStockMovementsByInventoryItemRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetStockMovementsByInventoryItemRequest, GetStockMovementsByInventoryItemQuery, GetStockMovementsByInventoryItemResponse>(request, context);
}
// ========== Reports ==========
public override async Task<GetInventorySummaryResponse> GetInventorySummary(GetInventorySummaryRequest request, ServerCallContext context)
{
var query = _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => !i.IsDeleted);
if (request.WarehouseId != null)
query = query.Where(i => i.WarehouseId == request.WarehouseId.Value);
var items = await query.ToListAsync(context.CancellationToken);
var regularProducts = items.Where(i => i.ProductType == Domain.Enums.ProductType.RegularProduct).ToList();
var discountProducts = items.Where(i => i.ProductType == Domain.Enums.ProductType.DiscountProduct).ToList();
long totalStockValue = items.Sum(i =>
{
long unitPrice = i.Product?.Price ?? i.DiscountProduct?.Price ?? 0;
return (long)i.Quantity * unitPrice;
});
return new GetInventorySummaryResponse
{
TotalProducts = regularProducts.Count,
TotalDiscountProducts = discountProducts.Count,
TotalQuantity = items.Sum(i => i.Quantity),
TotalReserved = items.Sum(i => i.ReservedQuantity),
LowStockCount = items.Count(i => i.Quantity <= i.LowStockThreshold && i.Quantity > 0),
OutOfStockCount = items.Count(i => i.Quantity == 0),
TotalStockValue = totalStockValue
};
}
public override async Task<GetStockValueReportResponse> GetStockValueReport(GetStockValueReportRequest request, ServerCallContext context)
{
var query = _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => !i.IsDeleted);
if (request.WarehouseId != null)
query = query.Where(i => i.WarehouseId == request.WarehouseId.Value);
if (request.ProductType != ProductType.Unspecified)
query = query.Where(i => i.ProductType == (Domain.Enums.ProductType)request.ProductType);
var dbItems = await query.ToListAsync(context.CancellationToken);
var items = dbItems.Select(i =>
{
long unitPrice = i.Product?.Price ?? i.DiscountProduct?.Price ?? 0;
string title = i.Product?.Title ?? i.DiscountProduct?.Title ?? string.Empty;
return new StockValueItem
{
ProductId = i.ProductId ?? i.DiscountProductId ?? 0,
ProductTitle = title,
ProductType = (ProductType)i.ProductType,
Quantity = i.Quantity,
UnitPrice = unitPrice,
TotalValue = (long)i.Quantity * unitPrice
};
}).ToList();
return new GetStockValueReportResponse
{
Items = { items },
TotalValue = items.Sum(i => i.TotalValue),
TotalItems = items.Count
};
}
}
@@ -1,3 +1,4 @@
using CMSMicroservice.Application.Common.Authorization;
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.ManualPaymentCQ.Commands.CreateManualPayment;
@@ -24,6 +25,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
_sender = sender;
}
[RequiresPermission(PermissionNames.ManualPaymentsCreate)]
public override async Task<CreateManualPaymentResponse> CreateManualPayment(
CreateManualPaymentRequest request,
ServerCallContext context)
@@ -37,6 +39,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
};
}
[RequiresPermission(PermissionNames.ManualPaymentsApprove)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> ApproveManualPayment(
ApproveManualPaymentRequest request,
ServerCallContext context)
@@ -44,6 +47,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
return await _dispatchRequestToCQRS.Handle<ApproveManualPaymentRequest, ApproveManualPaymentCommand>(request, context);
}
[RequiresPermission(PermissionNames.ManualPaymentsApprove)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> RejectManualPayment(
RejectManualPaymentRequest request,
ServerCallContext context)
@@ -51,6 +55,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
return await _dispatchRequestToCQRS.Handle<RejectManualPaymentRequest, RejectManualPaymentCommand>(request, context);
}
[RequiresPermission(PermissionNames.ManualPaymentsView)]
public override async Task<GetAllManualPaymentsResponse> GetAllManualPayments(
GetAllManualPaymentsRequest request,
ServerCallContext context)
@@ -58,6 +63,7 @@ public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractB
return await _dispatchRequestToCQRS.Handle<GetAllManualPaymentsRequest, GetAllManualPaymentsQuery, GetAllManualPaymentsResponse>(request, context);
}
[RequiresPermission(PermissionNames.ManualPaymentsCreate)]
public override async Task<ProcessManualMembershipPaymentResponse> ProcessManualMembershipPayment(
ProcessManualMembershipPaymentRequest request,
ServerCallContext context)
@@ -7,16 +7,27 @@ using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetUserNetworkPosi
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkMembershipHistory;
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
using CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using CMSMicroservice.Application.Common.Interfaces;
using Mapster;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.WebApi.Services;
public class NetworkMembershipService : NetworkMembershipContract.NetworkMembershipContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly ICurrentUserService _currentUserService;
public NetworkMembershipService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public NetworkMembershipService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
ICurrentUserService currentUserService)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_currentUserService = currentUserService;
}
public override async Task<Empty> JoinNetwork(JoinNetworkRequest request, ServerCallContext context)
@@ -54,4 +65,137 @@ public class NetworkMembershipService : NetworkMembershipContract.NetworkMembers
{
return await _dispatchRequestToCQRS.Handle<GetNetworkStatisticsRequest, GetNetworkStatisticsQuery, GetNetworkStatisticsResponse>(request, context);
}
// ============= Customer-specific Methods =============
public override async Task<GetMyNetworkTreeResponse> GetMyNetworkTree(GetMyNetworkTreeRequest request, ServerCallContext context)
{
// Use Customer-specific Query that gets UserId from ICurrentUserService
var query = new GetMyNetworkTreeQuery
{
MaxDepth = request.MaxDepth > 0 ? request.MaxDepth : 3
};
var tree = await _sender.Send(query, context.CancellationToken);
if (tree == null)
{
return new GetMyNetworkTreeResponse
{
RootNode = null,
TotalMembers = 0,
CurrentDepth = 0
};
}
// Convert NetworkTreeDto to NetworkTreeNodeModel
var rootNode = ConvertToNodeModel(tree);
return new GetMyNetworkTreeResponse
{
RootNode = rootNode,
TotalMembers = CountNodes(tree),
CurrentDepth = tree.CurrentDepth
};
}
public override async Task<GetMyNetworkTreeResponse> GetSubordinateTree(GetSubordinateTreeRequest request, ServerCallContext context)
{
// Get tree for a specific subordinate user
var query = new GetNetworkTreeQuery
{
UserId = request.TargetUserId,
MaxDepth = request.MaxDepth > 0 ? request.MaxDepth : 3
};
var tree = await _sender.Send(query, context.CancellationToken);
if (tree == null)
{
return new GetMyNetworkTreeResponse
{
RootNode = null,
TotalMembers = 0,
CurrentDepth = 0
};
}
var rootNode = ConvertToNodeModel(tree);
return new GetMyNetworkTreeResponse
{
RootNode = rootNode,
TotalMembers = CountNodes(tree),
CurrentDepth = tree.CurrentDepth
};
}
public override async Task<GetMyNetworkStatisticsResponse> GetMyNetworkStatistics(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
{
// Customer endpoint: resolve userId from JWT
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
var query = new GetNetworkStatisticsQuery { UserId = userId };
var stats = await _sender.Send(query, context.CancellationToken);
return stats.Adapt<GetMyNetworkStatisticsResponse>();
}
// Helper methods for tree conversion
private NetworkTreeNodeModel ConvertToNodeModel(NetworkTreeDto dto)
{
var node = new NetworkTreeNodeModel
{
UserId = dto.UserId,
UserName = $"{dto.FirstName} {dto.LastName}".Trim(),
FullName = $"{dto.FirstName} {dto.LastName}".Trim(),
NetworkLeg = dto.LegPosition.HasValue ? (int)dto.LegPosition.Value : 0,
NetworkLevel = dto.CurrentDepth,
Level = dto.CurrentDepth,
IsActive = dto.IsClubActive,
IsClubActive = dto.IsClubActive,
ReferralCode = dto.ReferralCode ?? "",
Mobile = dto.Mobile ?? "",
Position = dto.LegPosition.HasValue ? dto.LegPosition.Value.ToString() : "Root"
};
if (dto.ClubActivatedAt.HasValue)
{
node.ClubActivatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
DateTime.SpecifyKind(dto.ClubActivatedAt.Value, DateTimeKind.Utc));
}
if (dto.UserCreated != null)
{
node.UserCreated = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(
dto.UserCreated.UtcDateTime);
}
if (dto.ActivationWeekDefinitionId.HasValue)
{
node.ActivationWeekDefinitionId = dto.ActivationWeekDefinitionId.Value;
}
node.IsActivatedInTargetWeek = dto.IsActivatedInTargetWeek;
// Recursively convert children
if (dto.LeftChild != null)
{
node.LeftChild = ConvertToNodeModel(dto.LeftChild);
}
if (dto.RightChild != null)
{
node.RightChild = ConvertToNodeModel(dto.RightChild);
}
return node;
}
private int CountNodes(NetworkTreeDto tree)
{
if (tree == null) return 0;
return 1 + CountNodes(tree.LeftChild) + CountNodes(tree.RightChild);
}
}
@@ -10,14 +10,46 @@ using CMSMicroservice.Application.PackageCQ.Commands.VerifyBasePackagePayment;
using CMSMicroservice.Application.PackageCQ.Queries.GetPackage;
using CMSMicroservice.Application.PackageCQ.Queries.GetAllPackageByFilter;
using CMSMicroservice.Application.PackageCQ.Queries.GetUserPackageStatus;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackages;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPackageDetails;
using CMSMicroservice.Application.PackageCQ.Queries.GetCustomerPurchaseHistory;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Payment;
using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos;
using Microsoft.EntityFrameworkCore;
using MediatR;
using Mapster;
using Microsoft.Extensions.Configuration;
namespace CMSMicroservice.WebApi.Services;
public class PackageService : PackageContract.PackageContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
public PackageService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public PackageService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IPaymentGatewayService paymentGateway,
IConfiguration configuration)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_paymentGateway = paymentGateway;
_configuration = configuration;
}
public override async Task<CreateNewPackageResponse> CreateNewPackage(CreateNewPackageRequest request, ServerCallContext context)
{
@@ -65,4 +97,296 @@ public class PackageService : PackageContract.PackageContractBase
{
return await _dispatchRequestToCQRS.Handle<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponse>(request, context);
}
// ============= Customer-specific Method Implementations =============
public override async Task<GetCustomerPackagesResponse> GetCustomerPackages(GetCustomerPackagesRequest request, ServerCallContext context)
{
var query = new GetCustomerPackagesQuery
{
IncludeInactive = request.IncludeInactive,
PackageTypeFilter = request.PackageTypeFilter != PackageTypeEnum.PackageTypeBasic
? (int?)request.PackageTypeFilter
: null
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerPackagesResponse();
response.Models.AddRange(result.Adapt<List<CustomerPackageModel>>());
return response;
}
public override async Task<GetCustomerPackageDetailsResponse> GetCustomerPackageDetails(GetCustomerPackageDetailsRequest request, ServerCallContext context)
{
var query = new GetCustomerPackageDetailsQuery
{
PackageId = request.PackageId
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerPackageDetailsResponse
{
Id = result.Id,
Title = result.Title,
Description = result.Description,
Price = result.Price,
ImagePath = result.ImagePath
};
// Map Features
foreach (var feature in result.Features)
{
response.Features.Add(new PackageFeature
{
Title = feature.Title,
Description = feature.Description,
Icon = feature.Icon,
IsHighlighted = feature.IsHighlighted
});
}
// Map Requirements
response.Requirements = new PurchaseRequirements
{
RequiresMembership = result.Requirements.RequiresMembership,
MinimumWalletBalance = result.Requirements.MinimumWalletBalance
};
response.Requirements.Restrictions.AddRange(result.Requirements.Restrictions);
return response;
}
public override async Task<CustomerPurchasePackageResponse> CustomerPurchasePackage(CustomerPurchasePackageRequest request, ServerCallContext context)
{
var userId = GetCurrentUserId();
// Lookup package
var package = await _context.Packages
.AsNoTracking()
.Where(p => p.Id == request.PackageId && !p.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (package == null)
throw new RpcException(new Status(StatusCode.NotFound, "پکیج مورد نظر یافت نشد"));
// Create transaction
var transaction = new CMSMicroservice.Domain.Entities.Transaction
{
Amount = package.Price,
Description = $"خرید پکیج {package.Title}",
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
Type = Domain.Enums.TransactionType.Buy
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(context.CancellationToken);
// Create purchase record
var purchaseMethod = request.PurchaseMethod == PurchaseMethodEnum.PurchaseMethodGateway
? Domain.Enums.PackagePurchaseMethod.DirectPurchase
: Domain.Enums.PackagePurchaseMethod.DayaLoan;
var purchase = new CMSMicroservice.Domain.Entities.UserPackagePurchase
{
UserId = userId,
PackageId = package.Id,
PurchaseMethod = purchaseMethod,
PurchasedAt = DateTime.UtcNow,
Amount = package.Price,
TransactionId = transaction.Id
};
_context.UserPackagePurchases.Add(purchase);
await _context.SaveChangesAsync(context.CancellationToken);
// Initiate payment with gateway
var user = await _context.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new { u.Mobile })
.FirstOrDefaultAsync(context.CancellationToken);
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
Amount = package.Price,
UserId = userId,
Mobile = user?.Mobile ?? string.Empty,
Description = $"خرید پکیج {package.Title}",
CallbackUrl = request.CallbackUrl
}, context.CancellationToken);
if (!paymentResult.IsSuccess)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPurchasePackageResponse
{
Success = false,
Message = paymentResult.ErrorMessage ?? "خطا در ارتباط با درگاه پرداخت"
};
}
// Save RefId
transaction.RefId = paymentResult.RefId;
// ثبت PaymentTransaction
var paymentTx = new PaymentTransaction
{
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = package.Price,
CallbackUrl = request.CallbackUrl,
Description = $"خرید پکیج {package.Title}",
Mobile = user?.Mobile,
UserId = userId,
RequestStatusCode = 100,
RequestStatusMessage = "Success",
Authority = paymentResult.RefId,
PaymentStatus = false,
TransactionId = transaction.Id,
OrderId = purchase.Id.ToString()
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPurchasePackageResponse
{
Success = true,
Message = "درخواست خرید پکیج با موفقیت ثبت شد",
OrderId = purchase.Id,
PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty,
Authority = paymentResult.RefId ?? string.Empty
};
}
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(CustomerVerifyPackagePurchaseRequest request, ServerCallContext context)
{
// Find purchase record
var purchase = await _context.UserPackagePurchases
.Include(p => p.Package)
.Where(p => p.Id == request.OrderId && !p.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (purchase == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
// Find the associated transaction
var transaction = purchase.TransactionId.HasValue
? await _context.Transactions
.Where(t => t.Id == purchase.TransactionId.Value)
.FirstOrDefaultAsync(context.CancellationToken)
: null;
// If status from gateway callback is not OK
if (request.Status != "OK")
{
if (transaction != null)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
await _context.SaveChangesAsync(context.CancellationToken);
}
return new CustomerVerifyPackagePurchaseResponse
{
Success = false,
Message = "پرداخت توسط کاربر لغو شد"
};
}
// Verify with payment gateway
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, context.CancellationToken);
// آپدیت PaymentTransaction
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
}
if (verifyResult.IsSuccess && transaction != null)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow;
transaction.RefId = verifyResult.RefId;
}
else if (transaction != null)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
}
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerVerifyPackagePurchaseResponse
{
Success = verifyResult.IsSuccess,
Message = verifyResult.IsSuccess ? "خرید پکیج با موفقیت تایید شد" : (verifyResult.Message ?? "خرید پکیج ناموفق بود"),
TransactionId = transaction?.Id ?? 0,
ReferenceCode = verifyResult.RefId ?? string.Empty,
PurchaseInfo = verifyResult.IsSuccess ? new PackagePurchaseInfo
{
PackageId = purchase.PackageId,
PackageName = purchase.Package?.Title ?? string.Empty,
AmountPaid = purchase.Amount,
PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.PurchasedAt, DateTimeKind.Utc))
} : null
};
}
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
return userId;
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
}
public override async Task<GetCustomerPurchaseHistoryResponse> GetCustomerPurchaseHistory(GetCustomerPurchaseHistoryRequest request, ServerCallContext context)
{
var query = new GetCustomerPurchaseHistoryQuery
{
UserId = request.UserId,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
PackageTypeFilter = request.PackageTypeFilter != PackageTypeEnum.PackageTypeBasic
? (int?)request.PackageTypeFilter
: null,
FromDate = request.FromDate?.ToDateTime(),
ToDate = request.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerPurchaseHistoryResponse
{
MetaData = result.MetaData.Adapt<MetaData>()
};
foreach (var purchase in result.Purchases)
{
response.Purchases.Add(new PackagePurchaseHistory
{
Id = purchase.Id,
PackageId = purchase.PackageId,
PackageName = purchase.PackageName,
Amount = purchase.Amount,
PackageType = (PackageTypeEnum)purchase.PackageType,
PurchaseDate = Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.PurchaseDate, DateTimeKind.Utc)),
ExpiryDate = purchase.ExpiryDate.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(purchase.ExpiryDate.Value, DateTimeKind.Utc))
: null,
Status = (PaymentStatusEnum)purchase.Status,
StatusMessage = purchase.StatusMessage,
ReferenceCode = purchase.ReferenceCode
});
}
return response;
}
}
@@ -1,61 +1,691 @@
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.WebApi.Common.Services;
using Grpc.Core;
using MediatR;
using CMSMicroservice.Application.ProductsCQ.Commands.CreateNewProducts;
using CMSMicroservice.Application.ProductsCQ.Commands.UpdateProducts;
using CMSMicroservice.Application.ProductsCQ.Commands.DeleteProducts;
using CMSMicroservice.Application.ProductsCQ.Queries.GetProducts;
using CMSMicroservice.Application.ProductsCQ.Queries.GetAllProductsByFilter;
using CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductPrices;
using CMSMicroservice.Application.ProductsCQ.Commands.BulkUpdateProductStock;
using CMSMicroservice.Application.ProductsCQ.Queries.GetLowStockProducts;
using CMSMicroservice.Application.ProductsCQ.Commands.ToggleProductStatus;
using CMSMicroservice.Application.ProductsCQ.Commands.AddProductImage;
using CMSMicroservice.Application.ProductsCQ.Commands.RemoveProductImage;
using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProducts;
using CMSMicroservice.Application.ProductsCQ.Queries.GetCustomerProductsByFilter;
using CMSMicroservice.Application.ProductsCQ.Queries.GetProductGallery;
using CMSMicroservice.Application.Common.Interfaces;
using Mapster;
using Microsoft.EntityFrameworkCore;
using AppModels = CMSMicroservice.Application.Common.Models;
using System.Collections.Generic;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class ProductsService : ProductsContract.ProductsContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
public ProductsService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public ProductsService(ISender sender, IApplicationDbContext context)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
}
public override async Task<CreateNewProductsResponse> CreateNewProducts(CreateNewProductsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewProductsRequest, CreateNewProductsCommand, CreateNewProductsResponse>(request, context);
var command = new CreateNewProductsCommand
{
Title = request.Title,
Description = request.Description,
ShortInfomation = request.ShortInfomation,
FullInformation = request.FullInformation,
Price = request.Price,
Discount = request.Discount,
Rate = request.Rate,
ImagePath = request.ImagePath,
ThumbnailPath = request.ThumbnailPath,
SaleCount = request.SaleCount,
ViewCount = request.ViewCount,
RemainingCount = request.RemainingCount,
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName,
ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(),
ThumbnailFileMime = request.ThumbnailFile?.Mime,
ThumbnailFileName = request.ThumbnailFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new CreateNewProductsResponse { Id = result.Id };
}
public override async Task<Empty> UpdateProducts(UpdateProductsRequest request, ServerCallContext context)
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateProducts(UpdateProductsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateProductsRequest, UpdateProductsCommand>(request, context);
var command = new UpdateProductsCommand
{
Id = request.Id,
Title = request.Title,
Description = request.Description,
ShortInfomation = request.ShortInfomation,
FullInformation = request.FullInformation,
Price = request.Price,
Discount = request.Discount,
Rate = request.Rate,
ImagePath = request.ImagePath,
ThumbnailPath = request.ThumbnailPath,
SaleCount = request.SaleCount,
ViewCount = request.ViewCount,
RemainingCount = request.RemainingCount,
CategoryIds = request.CategoryIds?.ToList() ?? new List<long>(),
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName,
ThumbnailFileBytes = request.ThumbnailFile?.File?.ToByteArray(),
ThumbnailFileMime = request.ThumbnailFile?.Mime,
ThumbnailFileName = request.ThumbnailFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<Empty> DeleteProducts(DeleteProductsRequest request, ServerCallContext context)
public override async Task<Google.Protobuf.WellKnownTypes.Empty> DeleteProducts(DeleteProductsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteProductsRequest, DeleteProductsCommand>(request, context);
var command = new DeleteProductsCommand { Id = request.Id };
await _sender.Send(command, context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<GetProductsResponse> GetProducts(GetProductsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetProductsRequest, GetProductsQuery, GetProductsResponse>(request, context);
var query = new GetCustomerProductsQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetProductsResponse
{
Id = result.Id,
Title = result.Title,
Description = result.Description,
ShortInfomation = result.ShortInfomation,
FullInformation = result.FullInformation,
Price = result.Price,
Discount = result.Discount,
Rate = result.Rate,
ImagePath = result.ImagePath,
ThumbnailPath = result.ThumbnailPath,
SaleCount = result.SaleCount,
ViewCount = result.ViewCount,
RemainingCount = result.RemainingCount
};
if (result.Gallery != null)
{
foreach (var item in result.Gallery)
{
response.Gallery.Add(new ProductGalleryItem
{
ProductGalleryId = item.ProductGalleryId,
ProductImageId = item.ProductImageId,
Title = item.Title,
ImagePath = item.ImagePath,
ImageThumbnailPath = item.ImageThumbnailPath
});
}
}
if (result.Categories != null)
{
foreach (var cat in result.Categories)
{
var categoryPath = new ProductCategoryPath
{
CategoryId = cat.CategoryId,
Title = cat.Title
};
if (cat.Path != null)
{
foreach (var node in cat.Path)
{
categoryPath.Path.Add(new CategoryNode
{
Id = node.Id,
Title = node.Title,
ParentId = node.ParentId
});
}
}
response.Categories.Add(categoryPath);
}
}
return response;
}
public override async Task<GetAllProductsByFilterResponse> GetAllProductsByFilter(GetAllProductsByFilterRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllProductsByFilterRequest, GetAllProductsByFilterQuery, GetAllProductsByFilterResponse>(request, context);
// Map to GetCustomerProductsByFilter query (public products API)
var query = new GetCustomerProductsByFilterQuery
{
Id = request.Filter?.Id,
Title = request.Filter?.Title ?? string.Empty,
Description = request.Filter?.Description ?? string.Empty,
ShortInfomation = request.Filter?.ShortInfomation ?? string.Empty,
FullInformation = request.Filter?.FullInformation ?? string.Empty,
Price = request.Filter?.Price,
Discount = request.Filter?.Discount,
Rate = request.Filter?.Rate,
SaleCount = request.Filter?.SaleCount,
ViewCount = request.Filter?.ViewCount,
RemainingCount = request.Filter?.RemainingCount,
CategoryIds = request.Filter?.CategoryId.HasValue == true
? new List<long> { request.Filter.CategoryId.Value }
: new List<long>(),
IsActive = request.Filter?.IsActive,
SortBy = request.SortBy ?? string.Empty,
PaginationState = request.PaginationState != null
? new AppModels.PaginationState
{
PageNumber = request.PaginationState.PageNumber,
PageSize = request.PaginationState.PageSize
}
: new AppModels.PaginationState { PageNumber = 1, PageSize = 20 }
};
var result = await _sender.Send(query, context.CancellationToken);
return new GetAllProductsByFilterResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = result.MetaData.CurrentPage,
TotalPage = result.MetaData.TotalPage,
PageSize = result.MetaData.PageSize,
TotalCount = result.MetaData.TotalCount,
HasPrevious = result.MetaData.HasPrevious,
HasNext = result.MetaData.HasNext
},
Models = { result.Models.Select(m => new GetAllProductsByFilterResponseModel
{
Id = m.Id,
Title = m.Title,
Description = m.Description,
ShortInfomation = m.ShortInfomation,
FullInformation = m.FullInformation,
Price = m.Price,
Discount = m.Discount,
Rate = m.Rate,
ImagePath = m.ImagePath,
ThumbnailPath = m.ThumbnailPath,
SaleCount = m.SaleCount,
ViewCount = m.ViewCount,
RemainingCount = m.RemainingCount,
CategoryIds = { m.Categories?.Select(c => c.CategoryId) ?? Enumerable.Empty<long>() },
IsActive = m.IsActive
}) }
};
}
public override async Task<BulkUpdateProductPricesResponse> BulkUpdateProductPrices(BulkUpdateProductPricesRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<BulkUpdateProductPricesRequest, BulkUpdateProductPricesCommand, BulkUpdateProductPricesResponse>(request, context);
var response = new BulkUpdateProductPricesResponse { Total = request.Products.Count };
foreach (var item in request.Products)
{
try
{
var product = await _context.Products.FindAsync(new object[] { item.ProductId }, context.CancellationToken);
if (product == null)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = "محصول یافت نشد" });
continue;
}
product.Price = item.NewPrice;
if (item.NewDiscount != null) product.Discount = item.NewDiscount.Value;
if (item.NewClubDiscountPercent != null) product.ClubDiscountPercent = item.NewClubDiscountPercent.Value;
response.Succeeded++;
}
catch (Exception ex)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = ex.Message });
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return response;
}
public override async Task<BulkUpdateProductStockResponse> BulkUpdateProductStock(BulkUpdateProductStockRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<BulkUpdateProductStockRequest, BulkUpdateProductStockCommand, BulkUpdateProductStockResponse>(request, context);
var response = new BulkUpdateProductStockResponse { Total = request.Products.Count };
foreach (var item in request.Products)
{
try
{
var product = await _context.Products.FindAsync(new object[] { item.ProductId }, context.CancellationToken);
if (product == null)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = "محصول یافت نشد" });
continue;
}
product.RemainingCount = request.UpdateType switch
{
StockUpdateType.Set => item.Quantity,
StockUpdateType.Add => product.RemainingCount + item.Quantity,
StockUpdateType.Subtract => Math.Max(0, product.RemainingCount - item.Quantity),
_ => item.Quantity
};
response.Succeeded++;
}
catch (Exception ex)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = item.ProductId, ErrorMessage = ex.Message });
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return response;
}
public override async Task<GetLowStockProductsResponse> GetLowStockProducts(GetLowStockProductsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetLowStockProductsRequest, GetLowStockProductsQuery, GetLowStockProductsResponse>(request, context);
var threshold = request.Threshold > 0 ? request.Threshold : 10;
var pageIndex = request.PageIndex > 0 ? request.PageIndex : 1;
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
var query = _context.Products
.Where(p => !p.IsDeleted && p.RemainingCount <= threshold);
if (request.IsClubExclusive != null)
query = query.Where(p => p.IsClubExclusive == request.IsClubExclusive.Value);
var totalCount = await query.CountAsync(context.CancellationToken);
var products = await query
.OrderBy(p => p.RemainingCount)
.Skip((pageIndex - 1) * pageSize)
.Take(pageSize)
.Select(p => new LowStockProduct
{
Id = p.Id,
Title = p.Title,
RemainingCount = p.RemainingCount,
Price = p.Price,
IsClubExclusive = p.IsClubExclusive
})
.ToListAsync(context.CancellationToken);
return new GetLowStockProductsResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = pageIndex,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
HasPrevious = pageIndex > 1,
HasNext = pageIndex * pageSize < totalCount
},
Products = { products }
};
}
public override async Task<ToggleProductStatusResponse> ToggleProductStatus(ToggleProductStatusRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ToggleProductStatusRequest, ToggleProductStatusCommand, ToggleProductStatusResponse>(request, context);
var response = new ToggleProductStatusResponse { Total = request.ProductIds.Count };
foreach (var productId in request.ProductIds)
{
try
{
var product = await _context.Products.FindAsync(new object[] { productId }, context.CancellationToken);
if (product == null)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = productId, ErrorMessage = "محصول یافت نشد" });
continue;
}
product.IsDeleted = !request.Enable;
if (request.Enable && request.DefaultStock > 0 && product.RemainingCount == 0)
product.RemainingCount = request.DefaultStock;
response.Succeeded++;
}
catch (Exception ex)
{
response.Failed++;
response.Errors.Add(new BulkOperationError { ProductId = productId, ErrorMessage = ex.Message });
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return response;
}
// ============= Category-Product DragDrop Methods =============
public override async Task<GetProductsForCategoryResponse> GetProductsForCategory(GetProductsForCategoryRequest request, ServerCallContext context)
{
var assignedProductIds = await _context.ProductCategories
.Where(pc => pc.CategoryId == request.CategoryId && !pc.IsDeleted)
.Select(pc => pc.ProductId)
.ToListAsync(context.CancellationToken);
var allProducts = await _context.Products
.Where(p => !p.IsDeleted)
.OrderBy(p => p.Title)
.Select(p => new CategoryProductItem
{
Id = p.Id,
Title = p.Title,
Selected = assignedProductIds.Contains(p.Id)
})
.ToListAsync(context.CancellationToken);
return new GetProductsForCategoryResponse { Items = { allProducts } };
}
public override async Task<GetCategoriesResponse> GetCategories(GetCategoriesRequest request, ServerCallContext context)
{
var assignedCategoryIds = await _context.ProductCategories
.Where(pc => pc.ProductId == request.ProductId && !pc.IsDeleted)
.Select(pc => pc.CategoryId)
.ToListAsync(context.CancellationToken);
var allCategories = await _context.Categories
.Where(c => !c.IsDeleted && c.IsActive)
.OrderBy(c => c.SortOrder)
.Select(c => new CategoryItem
{
Id = c.Id,
Title = c.Title,
Selected = assignedCategoryIds.Contains(c.Id)
})
.ToListAsync(context.CancellationToken);
return new GetCategoriesResponse { Items = { allCategories } };
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateProductCategories(UpdateProductCategoriesRequest request, ServerCallContext context)
{
var existingLinks = await _context.ProductCategories
.Where(pc => pc.ProductId == request.ProductId)
.ToListAsync(context.CancellationToken);
// حذف لینک‌های قبلی
foreach (var link in existingLinks)
link.IsDeleted = true;
// ایجاد لینک‌های جدید
foreach (var categoryId in request.CategoryIds)
{
var existing = existingLinks.FirstOrDefault(l => l.CategoryId == categoryId);
if (existing != null)
{
existing.IsDeleted = false;
}
else
{
_context.ProductCategories.Add(new Domain.Entities.ProductCategory
{
ProductId = request.ProductId,
CategoryId = categoryId
});
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateCategoryProducts(UpdateCategoryProductsRequest request, ServerCallContext context)
{
var existingLinks = await _context.ProductCategories
.Where(pc => pc.CategoryId == request.CategoryId)
.ToListAsync(context.CancellationToken);
// حذف لینک‌های قبلی
foreach (var link in existingLinks)
link.IsDeleted = true;
// ایجاد لینک‌های جدید
foreach (var productId in request.ProductIds)
{
var existing = existingLinks.FirstOrDefault(l => l.ProductId == productId);
if (existing != null)
{
existing.IsDeleted = false;
}
else
{
_context.ProductCategories.Add(new Domain.Entities.ProductCategory
{
ProductId = productId,
CategoryId = request.CategoryId
});
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
// ============= Customer-specific Methods =============
public override async Task<GetProductsResponse> GetCustomerProducts(GetProductsRequest request, ServerCallContext context)
{
var query = new GetCustomerProductsQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetProductsResponse
{
Id = result.Id,
Title = result.Title,
Description = result.Description,
ShortInfomation = result.ShortInfomation,
FullInformation = result.FullInformation,
Price = result.Price,
Discount = result.Discount,
Rate = result.Rate,
ImagePath = result.ImagePath,
ThumbnailPath = result.ThumbnailPath,
SaleCount = result.SaleCount,
ViewCount = result.ViewCount,
RemainingCount = result.RemainingCount
};
// Add gallery items
if (result.Gallery != null)
{
foreach (var item in result.Gallery)
{
response.Gallery.Add(new ProductGalleryItem
{
ProductGalleryId = item.ProductGalleryId,
ProductImageId = item.ProductImageId,
Title = item.Title,
ImagePath = item.ImagePath,
ImageThumbnailPath = item.ImageThumbnailPath
});
}
}
// Add categories
if (result.Categories != null)
{
foreach (var cat in result.Categories)
{
var categoryPath = new ProductCategoryPath
{
CategoryId = cat.CategoryId,
Title = cat.Title
};
if (cat.Path != null)
{
foreach (var node in cat.Path)
{
categoryPath.Path.Add(new CategoryNode
{
Id = node.Id,
Title = node.Title,
ParentId = node.ParentId
});
}
}
response.Categories.Add(categoryPath);
}
}
return response;
}
public override async Task<GetCustomerProductsByFilterResponse> GetCustomerProductsByFilter(GetAllProductsByFilterRequest request, ServerCallContext context)
{
var query = new GetCustomerProductsByFilterQuery
{
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
SortBy = request.SortBy,
Id = request.Filter?.Id,
Title = request.Filter?.Title,
Description = request.Filter?.Description,
ShortInfomation = request.Filter?.ShortInfomation,
FullInformation = request.Filter?.FullInformation,
Price = request.Filter?.Price,
Discount = request.Filter?.Discount,
Rate = request.Filter?.Rate,
ImagePath = request.Filter?.ImagePath,
ThumbnailPath = request.Filter?.ThumbnailPath,
SaleCount = request.Filter?.SaleCount,
ViewCount = request.Filter?.ViewCount,
RemainingCount = request.Filter?.RemainingCount,
CategoryIds = request.Filter?.CategoryId != null ? new List<long> { request.Filter.CategoryId.Value } : null
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerProductsByFilterResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = result.MetaData.CurrentPage,
TotalPage = result.MetaData.TotalPage,
PageSize = result.MetaData.PageSize,
TotalCount = result.MetaData.TotalCount,
HasPrevious = result.MetaData.HasPrevious,
HasNext = result.MetaData.HasNext
}
};
foreach (var model in result.Models)
{
var productModel = new GetCustomerProductsByFilterResponseModel
{
Id = model.Id,
Title = model.Title,
Description = model.Description,
ShortInfomation = model.ShortInfomation,
FullInformation = model.FullInformation,
Price = model.Price,
Discount = model.Discount,
Rate = model.Rate,
ImagePath = model.ImagePath,
ThumbnailPath = model.ThumbnailPath,
SaleCount = model.SaleCount,
ViewCount = model.ViewCount,
RemainingCount = model.RemainingCount
};
if (model.Categories != null)
{
foreach (var cat in model.Categories)
{
var categoryPath = new ProductCategoryPath
{
CategoryId = cat.CategoryId,
Title = cat.Title
};
if (cat.Path != null)
{
foreach (var node in cat.Path)
{
categoryPath.Path.Add(new CategoryNode
{
Id = node.Id,
Title = node.Title,
ParentId = node.ParentId
});
}
}
productModel.Categories.Add(categoryPath);
}
}
response.Models.Add(productModel);
}
return response;
}
// ============= Product Image Management =============
public override async Task<AddProductImageResponse> AddProductImage(AddProductImageRequest request, ServerCallContext context)
{
var command = new AddProductImageCommand
{
ProductId = request.ProductId,
Title = request.Title,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new AddProductImageResponse
{
ProductGalleryId = result.ProductGalleryId,
ProductImageId = result.ProductImageId,
Title = result.Title,
ImagePath = result.ImagePath,
ImageThumbnailPath = result.ImageThumbnailPath
};
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> RemoveProductImage(RemoveProductImageRequest request, ServerCallContext context)
{
var command = new RemoveProductImageCommand { ProductGalleryId = request.ProductGalleryId };
await _sender.Send(command, context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<GetProductGalleryResponse> GetProductGallery(GetProductGalleryRequest request, ServerCallContext context)
{
var query = new GetProductGalleryQuery { ProductId = request.ProductId };
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetProductGalleryResponse();
foreach (var item in result.Items)
{
response.Items.Add(new ProductGalleryItem
{
ProductGalleryId = item.ProductGalleryId,
ProductImageId = item.ProductImageId,
Title = item.Title,
ImagePath = item.ImagePath,
ImageThumbnailPath = item.ImageThumbnailPath
});
}
return response;
}
}
@@ -0,0 +1,218 @@
using System.Linq;
using CMSMicroservice.Protobuf.Protos.SitePage;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePage;
using CMSMicroservice.Application.SitePageCQ.Commands.DeleteSitePage;
using CMSMicroservice.Application.SitePageCQ.Commands.UpdateSitePage;
using CMSMicroservice.Application.SitePageCQ.Commands.CreateSitePageSection;
using CMSMicroservice.Application.SitePageCQ.Commands.UpdateSitePageSection;
using CMSMicroservice.Application.SitePageCQ.Commands.DeleteSitePageSection;
using CMSMicroservice.Application.SitePageCQ.Commands.ReorderSitePageSections;
using CMSMicroservice.Application.SitePageCQ.Queries.GetSitePage;
using CMSMicroservice.Application.SitePageCQ.Queries.GetSitePageByKey;
using CMSMicroservice.Application.SitePageCQ.Queries.GetAllSitePages;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class SitePageService : SitePageContract.SitePageContractBase
{
private readonly ISender _sender;
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public SitePageService(ISender sender, IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_sender = sender;
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<GetSitePageResponse> GetSitePage(GetSitePageRequest request, ServerCallContext context)
{
var query = new GetSitePageQuery { Id = request.Id };
var result = await _sender.Send(query, context.CancellationToken);
return MapToResponse(result);
}
public override async Task<GetSitePageResponse> GetSitePageByKey(GetSitePageByKeyRequest request, ServerCallContext context)
{
var query = new GetSitePageByKeyQuery { PageKey = request.PageKey };
var result = await _sender.Send(query, context.CancellationToken);
return MapToResponse(result);
}
public override async Task<Empty> UpdateSitePage(UpdateSitePageRequest request, ServerCallContext context)
{
var command = new UpdateSitePageCommand
{
Id = request.Id,
Title = request.Title,
MetaDescription = request.MetaDescription,
HeroTitle = request.HeroTitle,
HeroSubtitle = request.HeroSubtitle,
HeroImagePath = request.HeroImagePath,
IsActive = request.IsActive,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<CreateSitePageResponse> CreateSitePage(CreateSitePageRequest request, ServerCallContext context)
{
var command = new CreateSitePageCommand
{
PageKey = request.PageKey,
Title = request.Title,
MetaDescription = request.MetaDescription,
HeroTitle = request.HeroTitle,
HeroSubtitle = request.HeroSubtitle,
IsActive = request.IsActive,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var id = await _sender.Send(command, context.CancellationToken);
return new CreateSitePageResponse { Id = id };
}
public override async Task<Empty> DeleteSitePage(DeleteSitePageRequest request, ServerCallContext context)
{
var command = new DeleteSitePageCommand { Id = request.Id };
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<GetAllSitePagesResponse> GetAllSitePages(GetAllSitePagesRequest request, ServerCallContext context)
{
var query = new GetAllSitePagesQuery();
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllSitePagesResponse();
foreach (var item in result)
{
response.Pages.Add(new SitePageSummary
{
Id = item.Id,
PageKey = item.PageKey ?? string.Empty,
Title = item.Title ?? string.Empty,
IsActive = item.IsActive,
SectionCount = item.SectionCount,
LastModified = item.LastModified.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(item.LastModified.Value, DateTimeKind.Utc))
: null
});
}
return response;
}
public override async Task<CreateSitePageSectionResponse> CreateSitePageSection(CreateSitePageSectionRequest request, ServerCallContext context)
{
var command = new CreateSitePageSectionCommand
{
SitePageId = request.SitePageId,
SectionKey = request.SectionKey,
Title = request.Title,
Subtitle = request.Subtitle,
HtmlContent = request.HtmlContent,
IconName = request.IconName,
ImagePath = request.ImagePath,
IsActive = true,
ExtraData = request.ExtraData,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
var result = await _sender.Send(command, context.CancellationToken);
return new CreateSitePageSectionResponse { Id = result };
}
public override async Task<Empty> UpdateSitePageSection(UpdateSitePageSectionRequest request, ServerCallContext context)
{
var command = new UpdateSitePageSectionCommand
{
Id = request.Id,
SectionKey = request.SectionKey,
Title = request.Title,
Subtitle = request.Subtitle,
HtmlContent = request.HtmlContent,
IconName = request.IconName,
ImagePath = request.ImagePath,
SortOrder = request.SortOrder,
IsActive = request.IsActive,
ExtraData = request.ExtraData,
ImageFileBytes = request.ImageFile?.File?.ToByteArray(),
ImageFileMime = request.ImageFile?.Mime,
ImageFileName = request.ImageFile?.FileName
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
public override async Task<Empty> DeleteSitePageSection(DeleteSitePageSectionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteSitePageSectionRequest, DeleteSitePageSectionCommand>(request, context);
}
public override async Task<Empty> ReorderSitePageSections(ReorderSitePageSectionsRequest request, ServerCallContext context)
{
var command = new ReorderSitePageSectionsCommand
{
Items = request.Items.Select(x => new Application.SitePageCQ.Commands.ReorderSitePageSections.SectionSortItem
{
Id = x.Id,
SortOrder = x.SortOrder
}).ToList()
};
await _sender.Send(command, context.CancellationToken);
return new Empty();
}
// ── Private Mapping Helpers ──
private static GetSitePageResponse MapToResponse(SitePageDto dto)
{
var response = new GetSitePageResponse
{
Id = dto.Id,
PageKey = dto.PageKey ?? string.Empty,
Title = dto.Title ?? string.Empty,
MetaDescription = dto.MetaDescription,
HeroTitle = dto.HeroTitle,
HeroSubtitle = dto.HeroSubtitle,
HeroImagePath = dto.HeroImagePath,
IsActive = dto.IsActive
};
if (dto.Sections != null)
{
foreach (var s in dto.Sections)
{
response.Sections.Add(new SitePageSectionItem
{
Id = s.Id,
SectionKey = s.SectionKey ?? string.Empty,
Title = s.Title ?? string.Empty,
Subtitle = s.Subtitle,
HtmlContent = s.HtmlContent,
IconName = s.IconName,
ImagePath = s.ImagePath,
SortOrder = s.SortOrder,
IsActive = s.IsActive,
ExtraData = s.ExtraData
});
}
}
return response;
}
}
@@ -7,15 +7,42 @@ using CMSMicroservice.Application.TransactionsCQ.Queries.GetTransactions;
using CMSMicroservice.Application.TransactionsCQ.Queries.GetAllTransactionsByFilter;
using CMSMicroservice.Application.TransactionsCQ.Commands.VerifyTransaction;
using CMSMicroservice.Application.TransactionsCQ.Commands.RefundTransaction;
using CMSMicroservice.Application.TransactionsCQ.Queries.GetCustomerTransaction;
using CMSMicroservice.Application.TransactionsCQ.Queries.GetCustomerTransactionsByFilter;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities.Payment;
using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using MediatR;
using Mapster;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class TransactionsService : TransactionsContract.TransactionsContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IPaymentGatewayService _paymentGateway;
private readonly IConfiguration _configuration;
public TransactionsService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public TransactionsService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IPaymentGatewayService paymentGateway,
IConfiguration configuration)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_paymentGateway = paymentGateway;
_configuration = configuration;
}
public override async Task<CreateNewTransactionsResponse> CreateNewTransactions(CreateNewTransactionsRequest request, ServerCallContext context)
{
@@ -47,4 +74,212 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
{
return await _dispatchRequestToCQRS.Handle<RefundTransactionRequest, RefundTransactionCommand, RefundTransactionResponse>(request, context);
}
// ============= Customer-specific Methods =============
public override async Task<GetCustomerTransactionResponse> GetCustomerTransaction(GetCustomerTransactionRequest request, ServerCallContext context)
{
var query = new GetCustomerTransactionQuery
{
Id = request.Id,
Authority = request.Authority,
UserId = 0 // از JWT دریافت می‌شود
};
var result = await _sender.Send(query, context.CancellationToken);
return new GetCustomerTransactionResponse
{
Id = result.Id,
Amount = result.Amount,
Description = result.Description,
PaymentStatus = result.PaymentStatus == Domain.Enums.PaymentStatus.Success,
RefId = result.RefId,
Type = (TransactionTypeEnum)result.Type,
Currency = CurrencyEnum.Irr
};
}
public override async Task<GetCustomerTransactionsByFilterResponse> GetCustomerTransactionsByFilter(GetCustomerTransactionsByFilterRequest request, ServerCallContext context)
{
var query = new GetCustomerTransactionsByFilterQuery
{
UserId = 0, // از JWT دریافت می‌شود
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
SortBy = request.SortBy,
IdFilter = request.Filter?.Id,
AmountFilter = request.Filter?.Amount,
DescriptionFilter = request.Filter?.Description,
PaymentStatusFilter = request.Filter?.PaymentStatus,
RefIdFilter = request.Filter?.RefId,
TypeFilter = request.Filter?.Type != null ? (int?)request.Filter.Type : null
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerTransactionsByFilterResponse
{
MetaData = result.MetaData.Adapt<CMSMicroservice.Protobuf.Protos.MetaData>()
};
foreach (var model in result.Models)
{
response.Models.Add(new GetCustomerTransactionsByFilterResponseModel
{
Id = model.Id,
Amount = model.Amount,
Description = model.Description,
PaymentStatus = model.PaymentStatus == Domain.Enums.PaymentStatus.Success,
RefId = model.RefId,
Type = (TransactionTypeEnum)model.Type,
Currency = CurrencyEnum.Irr
});
}
return response;
}
public override async Task<CustomerPaymentRequestResponse> CustomerPaymentRequest(CustomerPaymentRequestRequest request, ServerCallContext context)
{
var userId = GetCurrentUserId();
// Get user mobile for payment gateway
var user = await _context.Users
.AsNoTracking()
.Where(u => u.Id == userId)
.Select(u => new { u.Mobile, u.Email })
.FirstOrDefaultAsync(context.CancellationToken);
// Create transaction record in DB
var transaction = new CMSMicroservice.Domain.Entities.Transaction
{
Amount = request.Amount,
Description = request.Description ?? "پرداخت آنلاین",
PaymentStatus = Domain.Enums.PaymentStatus.Pending,
Type = Domain.Enums.TransactionType.DepositIpg
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(context.CancellationToken);
// Initiate payment with gateway
var paymentResult = await _paymentGateway.InitiatePaymentAsync(new PaymentRequest
{
Amount = request.Amount,
UserId = userId,
Mobile = request.Mobile ?? user?.Mobile ?? string.Empty,
Description = request.Description ?? "پرداخت آنلاین",
CallbackUrl = request.CallbackUrl
}, context.CancellationToken);
if (!paymentResult.IsSuccess)
{
// Update transaction status to failed
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
await _context.SaveChangesAsync(context.CancellationToken);
throw new RpcException(new Status(StatusCode.Internal,
paymentResult.ErrorMessage ?? "خطا در ارتباط با درگاه پرداخت"));
}
// Save RefId from gateway
transaction.RefId = paymentResult.RefId;
// ثبت PaymentTransaction
var paymentTx = new PaymentTransaction
{
GatewayProvider = _configuration["PaymentProvider"] ?? "zarinpal",
MerchantId = _configuration["ZarinPal:MerchantId"] ?? "",
Amount = request.Amount,
CallbackUrl = request.CallbackUrl,
Description = request.Description ?? "پرداخت آنلاین",
Mobile = request.Mobile ?? user?.Mobile,
UserId = userId,
RequestStatusCode = 100,
RequestStatusMessage = "Success",
Authority = paymentResult.RefId,
PaymentStatus = false,
TransactionId = transaction.Id
};
_context.PaymentTransactions.Add(paymentTx);
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPaymentRequestResponse
{
PaymentGWUrl = paymentResult.GatewayUrl ?? string.Empty
};
}
public override async Task<CustomerPaymentVerificationResponse> CustomerPaymentVerification(CustomerPaymentVerificationRequest request, ServerCallContext context)
{
// Find the transaction by authority/refId
var transaction = await _context.Transactions
.Where(t => t.RefId == request.Authority && !t.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (transaction == null)
throw new RpcException(new Status(StatusCode.NotFound, "تراکنش یافت نشد"));
// If status from gateway callback is not OK, mark as rejected
if (request.Status != "OK")
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPaymentVerificationResponse
{
Id = transaction.Id,
PaymentStatus = false,
Message = "پرداخت توسط کاربر لغو شد",
VerificationStatusCode = -1
};
}
// Verify with gateway
var verifyResult = await _paymentGateway.VerifyPaymentAsync(
request.Authority, request.Status, context.CancellationToken);
// آپدیت PaymentTransaction
var paymentTx = await _context.PaymentTransactions
.FirstOrDefaultAsync(pt => pt.Authority == request.Authority, context.CancellationToken);
if (paymentTx != null)
{
paymentTx.PaymentStatus = verifyResult.IsSuccess;
paymentTx.VerificationStatusCode = verifyResult.VerificationCode;
paymentTx.VerificationStatusMessage = verifyResult.Message;
paymentTx.CardPan = verifyResult.CardPan;
paymentTx.CardHash = verifyResult.CardHash;
paymentTx.RefId = verifyResult.TrackingCode;
}
if (verifyResult.IsSuccess)
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Success;
transaction.PaymentDate = DateTime.UtcNow;
transaction.RefId = verifyResult.RefId;
}
else
{
transaction.PaymentStatus = Domain.Enums.PaymentStatus.Reject;
}
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPaymentVerificationResponse
{
Id = transaction.Id,
PaymentStatus = verifyResult.IsSuccess,
Message = verifyResult.IsSuccess ? "پرداخت با موفقیت انجام شد" : (verifyResult.Message ?? "پرداخت ناموفق"),
RefId = verifyResult.RefId ?? string.Empty,
OrderId = string.Empty,
VerificationStatusCode = verifyResult.IsSuccess ? 100 : -1
};
}
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
return userId;
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
}
}
@@ -6,15 +6,28 @@ using CMSMicroservice.Application.UserAddressCQ.Commands.DeleteUserAddress;
using CMSMicroservice.Application.UserAddressCQ.Queries.GetUserAddress;
using CMSMicroservice.Application.UserAddressCQ.Queries.GetAllUserAddressByFilter;
using CMSMicroservice.Application.UserAddressCQ.Commands.SetAddressAsDefault;
using CMSMicroservice.Application.UserAddressCQ.Queries.GetCustomerAddresses;
using CMSMicroservice.Application.UserAddressCQ.Commands.CreateCustomerAddress;
using CMSMicroservice.Application.UserAddressCQ.Commands.UpdateCustomerAddress;
using CMSMicroservice.Application.UserAddressCQ.Commands.DeleteCustomerAddress;
using CMSMicroservice.Application.UserAddressCQ.Commands.SetCustomerDefaultAddress;
using MediatR;
namespace CMSMicroservice.WebApi.Services;
public class UserAddressService : UserAddressContract.UserAddressContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
public UserAddressService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public UserAddressService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
}
#region Admin Methods
public override async Task<CreateNewUserAddressResponse> CreateNewUserAddress(CreateNewUserAddressRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewUserAddressRequest, CreateNewUserAddressCommand, CreateNewUserAddressResponse>(request, context);
@@ -39,4 +52,96 @@ public class UserAddressService : UserAddressContract.UserAddressContractBase
{
return await _dispatchRequestToCQRS.Handle<SetAddressAsDefaultRequest, SetAddressAsDefaultCommand>(request, context);
}
#endregion
#region Customer Methods
public override async Task<GetCustomerAddressesResponse> GetCustomerAddresses(GetCustomerAddressesRequest request, ServerCallContext context)
{
var query = new GetCustomerAddressesQuery();
var result = await _sender.Send(query);
var response = new GetCustomerAddressesResponse();
foreach (var addr in result.Addresses)
{
response.Models.Add(new CMSMicroservice.Protobuf.Protos.UserAddress.CustomerAddressModel
{
Id = addr.Id,
Title = addr.Title,
Address = addr.Address,
PostalCode = addr.PostalCode,
IsDefault = addr.IsDefault,
CityId = addr.CityId,
CityName = addr.CityName,
ProvinceName = addr.ProvinceName
});
}
return response;
}
public override async Task<CreateCustomerAddressResponse> CreateCustomerAddress(CreateCustomerAddressRequest request, ServerCallContext context)
{
var command = new CreateCustomerAddressCommand
{
Title = request.Title,
Address = request.Address,
PostalCode = request.PostalCode,
IsDefault = request.IsDefault,
CityId = request.CityId
};
var result = await _sender.Send(command);
return new CreateCustomerAddressResponse
{
Id = result.Id,
Message = result.Message
};
}
public override async Task<Empty> UpdateCustomerAddress(UpdateCustomerAddressRequest request, ServerCallContext context)
{
var command = new UpdateCustomerAddressCommand
{
Id = request.Id,
Title = request.Title,
Address = request.Address,
PostalCode = request.PostalCode,
IsDefault = request.IsDefault,
CityId = request.CityId
};
await _sender.Send(command);
return new Empty();
}
public override async Task<Empty> DeleteCustomerAddress(DeleteCustomerAddressRequest request, ServerCallContext context)
{
var command = new DeleteCustomerAddressCommand
{
Id = request.Id
};
await _sender.Send(command);
return new Empty();
}
public override async Task<Empty> SetCustomerDefaultAddress(SetCustomerDefaultAddressRequest request, ServerCallContext context)
{
var command = new SetCustomerDefaultAddressCommand
{
Id = request.Id
};
await _sender.Send(command);
return new Empty();
}
#endregion
}
@@ -1,44 +1,238 @@
using CMSMicroservice.Application.DiscountShopCQ.Commands.AddToCustomerCart;
using CMSMicroservice.Application.DiscountShopCQ.Commands.RemoveFromCustomerCart;
using CMSMicroservice.Application.DiscountShopCQ.Commands.UpdateCustomerCartItem;
using CMSMicroservice.Application.DiscountShopCQ.Queries.GetCustomerCart;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.UserCarts;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.UserCartsCQ.Commands.CreateNewUserCarts;
using CMSMicroservice.Application.UserCartsCQ.Commands.UpdateUserCarts;
using CMSMicroservice.Application.UserCartsCQ.Commands.DeleteUserCarts;
using CMSMicroservice.Application.UserCartsCQ.Queries.GetUserCarts;
using CMSMicroservice.Application.UserCartsCQ.Queries.GetAllUserCartsByFilter;
using CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
using Google.Protobuf.WellKnownTypes;
using Microsoft.EntityFrameworkCore;
using MediatR;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class UserCartsService : UserCartsContract.UserCartsContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly IDispatchRequestToCQRS _dispatcher;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
public UserCartsService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public UserCartsService(IDispatchRequestToCQRS dispatcher, ISender sender, IApplicationDbContext context)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_dispatcher = dispatcher;
_sender = sender;
_context = context;
}
public override async Task<CreateNewUserCartsResponse> CreateNewUserCarts(CreateNewUserCartsRequest request, ServerCallContext context)
#region Customer Methods
public override async Task<AddNewUserCartForCustomerResponse> AddNewUserCartForCustomer(
AddNewUserCartForCustomerRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewUserCartsRequest, CreateNewUserCartsCommand, CreateNewUserCartsResponse>(request, context);
var command = new AddToCustomerCartCommand
{
ProductId = request.ProductId,
Count = request.Count
};
var result = await _sender.Send(command);
return new AddNewUserCartForCustomerResponse
{
Id = result.Id,
Message = result.Message,
Success = result.Success
};
}
public override async Task<Empty> UpdateUserCarts(UpdateUserCartsRequest request, ServerCallContext context)
public override async Task<UpdateUserCartForCustomerResponse> UpdateUserCartForCustomer(
UpdateUserCartForCustomerRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateUserCartsRequest, UpdateUserCartsCommand>(request, context);
var command = new UpdateCustomerCartItemCommand
{
CartItemId = request.CartItemId,
Count = request.Count
};
var result = await _sender.Send(command);
return new UpdateUserCartForCustomerResponse
{
Message = result.Message,
Success = result.Success
};
}
public override async Task<Empty> DeleteUserCarts(DeleteUserCartsRequest request, ServerCallContext context)
public override async Task<RemoveUserCartForCustomerResponse> RemoveUserCartForCustomer(
RemoveUserCartForCustomerRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteUserCartsRequest, DeleteUserCartsCommand>(request, context);
var command = new RemoveFromCustomerCartCommand
{
CartItemId = request.CartItemId
};
var result = await _sender.Send(command);
return new RemoveUserCartForCustomerResponse
{
Message = result.Message,
Success = result.Success
};
}
public override async Task<GetUserCartsResponse> GetUserCarts(GetUserCartsRequest request, ServerCallContext context)
public override async Task<GetUserCartForCustomerResponse> GetCustomerCart(
GetUserCartForCustomerRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserCartsRequest, GetUserCartsQuery, GetUserCartsResponse>(request, context);
var query = new GetCustomerCartQuery();
var result = await _sender.Send(query);
var response = new GetUserCartForCustomerResponse
{
TotalPrice = result.TotalPrice,
TotalItemsCount = result.TotalItemsCount,
Message = result.Message
};
foreach (var item in result.Items)
{
response.Models.Add(new UserCartItem
{
Id = item.Id,
ProductId = item.ProductId,
ProductTitle = item.ProductTitle,
ProductShortInformation = item.ProductShortInformation,
ProductShortInfomation = item.ProductShortInformation, // Alias for typo compatibility
ProductPrice = item.ProductPrice,
ProductDiscount = item.ProductDiscount,
ProductThumbnailPath = item.ProductThumbnailPath,
Count = item.Count,
TotalItemPrice = item.TotalItemPrice,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(item.Created, DateTimeKind.Utc))
});
}
return response;
}
public override async Task<GetAllUserCartsByFilterResponse> GetAllUserCartsByFilter(GetAllUserCartsByFilterRequest request, ServerCallContext context)
#endregion
#region Admin Methods
public override async Task<AddNewUserCartResponse> AddNewUserCart(
AddNewUserCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllUserCartsByFilterRequest, GetAllUserCartsByFilterQuery, GetAllUserCartsByFilterResponse>(request, context);
var entity = new Domain.Entities.UserCart
{
ProductId = request.ProductId,
UserId = request.UserId,
Count = request.Count
};
_context.UserCarts.Add(entity);
await _context.SaveChangesAsync(context.CancellationToken);
return new AddNewUserCartResponse { Id = entity.Id };
}
public override async Task<ClearCartResponse> ClearCart(ClearCartRequest request, ServerCallContext context)
public override async Task<Empty> UpdateUserCart(
UpdateUserCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ClearCartRequest, ClearCartCommand, ClearCartResponse>(request, context);
var cartId = request.Id > 0 ? request.Id : request.UserCartId;
var cart = await _context.UserCarts.FindAsync(new object[] { cartId }, context.CancellationToken);
if (cart == null)
throw new RpcException(new Status(StatusCode.NotFound, "آیتم سبد خرید یافت نشد"));
cart.Count = request.Count;
await _context.SaveChangesAsync(context.CancellationToken);
return new Empty();
}
public override async Task<Empty> DeleteUserCart(
DeleteUserCartRequest request, ServerCallContext context)
{
var cart = await _context.UserCarts.FindAsync(new object[] { request.Id }, context.CancellationToken);
if (cart == null)
throw new RpcException(new Status(StatusCode.NotFound, "آیتم سبد خرید یافت نشد"));
cart.IsDeleted = true;
await _context.SaveChangesAsync(context.CancellationToken);
return new Empty();
}
public override async Task<GetUserCartResponse> GetUserCart(
GetUserCartRequest request, ServerCallContext context)
{
var cart = await _context.UserCarts
.Where(c => c.Id == request.Id && !c.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (cart == null)
throw new RpcException(new Status(StatusCode.NotFound, "آیتم سبد خرید یافت نشد"));
return new GetUserCartResponse
{
Id = cart.Id,
ProductId = cart.ProductId,
UserId = cart.UserId,
Count = cart.Count
};
}
public override async Task<GetAllUserCartsByFilterResponse> GetAllUserCartsByFilter(
GetAllUserCartsByFilterRequest request, ServerCallContext context)
{
var pageNumber = request.PaginationState?.PageNumber ?? 1;
var pageSize = request.PaginationState?.PageSize ?? 20;
if (pageNumber < 1) pageNumber = 1;
if (pageSize < 1) pageSize = 20;
var query = _context.UserCarts
.Include(c => c.Product)
.Where(c => !c.IsDeleted);
if (request.Filter?.Id != null)
query = query.Where(c => c.Id == request.Filter.Id.Value);
if (request.Filter?.ProductId != null)
query = query.Where(c => c.ProductId == request.Filter.ProductId.Value);
if (request.Filter?.UserId != null)
query = query.Where(c => c.UserId == request.Filter.UserId.Value);
var totalCount = await query.CountAsync(context.CancellationToken);
var items = await query
.OrderByDescending(c => c.Created)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(c => new GetAllUserCartsByFilterResponseModel
{
Id = c.Id,
ProductId = c.ProductId,
UserId = c.UserId,
Count = c.Count,
ProductTitle = c.Product != null ? c.Product.Title : string.Empty,
ProductShortInfomation = c.Product != null ? (c.Product.ShortInfomation ?? string.Empty) : string.Empty,
ProductPrice = c.Product != null ? c.Product.Price : 0,
ProductDiscount = c.Product != null ? c.Product.Discount : 0,
ProductThumbnailPath = c.Product != null ? (c.Product.ThumbnailPath ?? string.Empty) : string.Empty,
Created = Timestamp.FromDateTime(DateTime.SpecifyKind(c.Created, DateTimeKind.Utc))
})
.ToListAsync(context.CancellationToken);
return new GetAllUserCartsByFilterResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = pageNumber,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
HasPrevious = pageNumber > 1,
HasNext = pageNumber * pageSize < totalCount
},
Models = { items }
};
}
#endregion
}
@@ -0,0 +1,158 @@
using CMSMicroservice.Protobuf.Protos.UserCarts;
using CMSMicroservice.WebApi.Common.Services;
namespace CMSMicroservice.WebApi.Services;
public class UserCartsService : UserCartsContract.UserCartsContractBase
{
private readonly IDispatchRequestToCQRS _dispatcher;
public UserCartsService(IDispatchRequestToCQRS dispatcher)
{
_dispatcher = dispatcher;
}
#region Customer Methods
public override async Task<AddNewUserCartForCustomerResponse> AddNewUserCartForCustomer(
AddNewUserCartForCustomerRequest request, ServerCallContext context)
{
// TODO: Map to DiscountShop AddToCart command
return new AddNewUserCartForCustomerResponse
{
Message = "AddNewUserCartForCustomer not implemented yet"
};
}
public override async Task<UpdateUserCartForCustomerResponse> UpdateUserCartForCustomer(
UpdateUserCartForCustomerRequest request, ServerCallContext context)
{
// TODO: Map to DiscountShop UpdateCartItemCount command
return new UpdateUserCartForCustomerResponse
{
Message = "UpdateUserCartForCustomer not implemented yet"
};
}
public override async Task<RemoveUserCartForCustomerResponse> RemoveUserCartForCustomer(
RemoveUserCartForCustomerRequest request, ServerCallContext context)
{
// TODO: Map to DiscountShop RemoveFromCart command
return new RemoveUserCartForCustomerResponse
{
Message = "RemoveUserCartForCustomer not implemented yet"
};
}
public override async Task<GetUserCartForCustomerResponse> GetUserCartForCustomer(
GetUserCartForCustomerRequest request, ServerCallContext context)
{
// TODO: Map to DiscountShop GetUserCart query
return new GetUserCartForCustomerResponse();
}
#endregion
#region Admin Methods
public override async Task<AddNewUserCartResponse> AddNewUserCart(
AddNewUserCartRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "AddNewUserCart not implemented yet"));
}
public override async Task<Empty> UpdateUserCart(
UpdateUserCartRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "UpdateUserCart not implemented yet"));
}
public override async Task<Empty> DeleteUserCart(
DeleteUserCartRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "DeleteUserCart not implemented yet"));
}
public override async Task<GetUserCartResponse> GetUserCart(
GetUserCartRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "GetUserCart not implemented yet"));
}
public override async Task<GetAllUserCartsByFilterResponse> GetAllUserCartsByFilter(
GetAllUserCartsByFilterRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "GetAllUserCartsByFilter not implemented yet"));
}
#endregion
}
using CMSMicroservice.Application.UserCartsCQ.Commands.ClearCart;
using Grpc.Core;
namespace CMSMicroservice.WebApi.Services;
public class UserCartsService : UserCartsContract.UserCartsContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public UserCartsService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<CreateNewUserCartsResponse> CreateNewUserCarts(CreateNewUserCartsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewUserCartsRequest, CreateNewUserCartsCommand, CreateNewUserCartsResponse>(request, context);
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateUserCarts(UpdateUserCartsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateUserCartsRequest, UpdateUserCartsCommand>(request, context);
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> DeleteUserCarts(DeleteUserCartsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteUserCartsRequest, DeleteUserCartsCommand>(request, context);
}
public override async Task<GetUserCartsResponse> GetUserCarts(GetUserCartsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserCartsRequest, GetUserCartsQuery, GetUserCartsResponse>(request, context);
}
public override async Task<GetAllUserCartsByFilterResponse> GetAllUserCartsByFilter(GetAllUserCartsByFilterRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllUserCartsByFilterRequest, GetAllUserCartsByFilterQuery, GetAllUserCartsByFilterResponse>(request, context);
}
public override async Task<ClearCartResponse> ClearCart(ClearCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ClearCartRequest, ClearCartCommand, ClearCartResponse>(request, context);
}
public override async Task<ClearCartResponse> ClearCartForCustomer(ClearCartRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ClearCartRequest, ClearCartCommand, ClearCartResponse>(request, context);
}
// ============= Customer-specific Methods =============
public override async Task<CreateNewUserCartsResponse> AddNewUserCartForCustomer(CreateNewUserCartsRequest request, ServerCallContext context)
{
// Reuse the existing CreateNewUserCarts logic
return await CreateNewUserCarts(request, context);
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateUserCartForCustomer(UpdateUserCartsRequest request, ServerCallContext context)
{
// Reuse the existing UpdateUserCarts logic
return await UpdateUserCarts(request, context);
}
public override async Task<GetAllUserCartsByFilterResponse> GetUserCartForCustomer(GetAllUserCartsByFilterRequest request, ServerCallContext context)
{
// Reuse the existing GetAllUserCartsByFilter logic
return await GetAllUserCartsByFilter(request, context);
}
}
@@ -1,73 +1,959 @@
using CMSMicroservice.Application.Common.Authorization;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.UserOrderCQ.Commands.CreateNewUserOrder;
using CMSMicroservice.Application.UserOrderCQ.Commands.UpdateUserOrder;
using CMSMicroservice.Application.UserOrderCQ.Commands.DeleteUserOrder;
using CMSMicroservice.Application.UserOrderCQ.Commands.UpdateOrderStatus;
using CMSMicroservice.Application.UserOrderCQ.Commands.ApplyDiscountToOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange;
using CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV;
using CMSMicroservice.Application.UserOrderCQ.Commands.SubmitShopBuyOrder;
using CMSMicroservice.Application.UserOrderCQ.Commands.CancelOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrders;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrder;
using CMSMicroservice.Application.UserOrderCQ.Queries.GetCustomerOrderHistory;
using CMSMicroservice.Application.OrderManagementCQ.Commands.UpdateOrderStatus;
using CMSMicroservice.Application.OrderManagementCQ.Commands.CancelOrderByAdmin;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Entities.Order;
using CMSMicroservice.Domain.Enums;
using AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using Google.Protobuf.WellKnownTypes;
using System.Collections.Generic;
using System.Linq;
using CMSMicroservice.Protobuf.Protos;
using MediatR;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class UserOrderService : UserOrderContract.UserOrderContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
public UserOrderService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public UserOrderService(ISender sender, IApplicationDbContext context, ICurrentUserService currentUserService)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
}
[RequiresPermission(PermissionNames.OrdersCreate)]
public override async Task<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewUserOrderRequest, CreateNewUserOrderCommand, CreateNewUserOrderResponse>(request, context);
var order = new UserOrder
{
Amount = request.Amount,
PackageId = request.PackageId > 0 ? request.PackageId : null,
TransactionId = request.TransactionId,
PaymentStatus = request.HasPaymentStatus
? (Domain.Enums.PaymentStatus)(int)request.PaymentStatus
: Domain.Enums.PaymentStatus.Pending,
PaymentDate = request.PaymentDate?.ToDateTime(),
UserId = request.UserId,
UserAddressId = request.UserAddressId,
PaymentMethod = request.HasPaymentMethod
? (Domain.Enums.PaymentMethod)(int)request.PaymentMethod
: null,
DeliveryStatus = Domain.Enums.DeliveryStatus.Pending
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(context.CancellationToken);
return new CreateNewUserOrderResponse { Id = order.Id };
}
public override async Task<Empty> UpdateUserOrder(UpdateUserOrderRequest request, ServerCallContext context)
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateUserOrder(UpdateUserOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateUserOrderRequest, UpdateUserOrderCommand>(request, context);
var order = await _context.UserOrders.FindAsync(new object[] { request.Id }, context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
if (request.Amount != null) order.Amount = request.Amount.Value;
if (request.PackageId != null) order.PackageId = request.PackageId.Value;
if (request.TransactionId != null) order.TransactionId = request.TransactionId.Value;
if (request.HasPaymentStatus) order.PaymentStatus = (Domain.Enums.PaymentStatus)(int)request.PaymentStatus;
if (request.PaymentDate != null) order.PaymentDate = request.PaymentDate.ToDateTime();
if (request.UserId != null) order.UserId = request.UserId.Value;
if (request.UserAddressId != null) order.UserAddressId = request.UserAddressId.Value;
if (request.HasPaymentMethod) order.PaymentMethod = (Domain.Enums.PaymentMethod)(int)request.PaymentMethod;
if (request.HasDeliveryStatus) order.DeliveryStatus = (Domain.Enums.DeliveryStatus)(int)request.DeliveryStatus;
if (request.TrackingCode != null) order.TrackingCode = request.TrackingCode;
if (request.DeliveryDescription != null) order.DeliveryDescription = request.DeliveryDescription;
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
public override async Task<Empty> DeleteUserOrder(DeleteUserOrderRequest request, ServerCallContext context)
[RequiresPermission(PermissionNames.OrdersDelete)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> DeleteUserOrder(DeleteUserOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteUserOrderRequest, DeleteUserOrderCommand>(request, context);
var order = await _context.UserOrders.FindAsync(new object[] { request.Id }, context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
order.IsDeleted = true;
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<GetUserOrderResponse> GetUserOrder(GetUserOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserOrderRequest, GetUserOrderQuery, GetUserOrderResponse>(request, context);
var query = new GetCustomerOrderQuery
{
OrderId = request.Id,
UserId = 0 // Admin: no user filter, can view any order by ID
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetUserOrderResponse
{
Id = result.Id,
Amount = result.Amount,
PackageId = result.PackageId ?? 0,
TransactionId = result.TransactionId,
UserId = result.UserId,
UserAddressId = result.UserAddressId,
UserAddressText = result.UserAddressText,
TrackingCode = result.TrackingCode,
DeliveryDescription = result.DeliveryDescription,
UserFullName = result.UserFullName,
UserNationalCode = result.UserNationalCode
};
// VAT Info
if (result.VatAmount > 0)
{
response.VatInfo = new OrderVATInfo
{
VatRate = result.VatPercentage / 100,
BaseAmount = result.Amount - result.VatAmount,
VatAmount = result.VatAmount,
TotalAmount = result.Amount,
IsPaid = result.PaymentStatus == Domain.Enums.PaymentStatus.Success
};
}
response.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)result.PaymentStatus;
if (result.PaymentDate.HasValue)
response.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(result.PaymentDate.Value, DateTimeKind.Utc));
if (result.PaymentMethod.HasValue)
response.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)result.PaymentMethod.Value;
response.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)result.DeliveryStatus;
foreach (var fd in result.FactorDetails)
{
response.FactorDetails.Add(new GetUserOrderResponseFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.ProductTitle,
ProductThumbnailPath = fd.ProductThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
});
}
return response;
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<GetAllUserOrderByFilterResponse> GetAllUserOrderByFilter(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllUserOrderByFilterRequest, GetAllUserOrderByFilterQuery, GetAllUserOrderByFilterResponse>(request, context);
// Admin API - can view all orders or filter by specific user
var query = new GetCustomerOrdersQuery
{
UserId = request.Filter?.UserId ?? 0, // 0 means all users (admin view)
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
PaymentStatusFilter = request.Filter?.HasPaymentStatus == true
? (int?)request.Filter.PaymentStatus
: null,
DeliveryStatusFilter = request.Filter?.HasDeliveryStatus == true
? (int?)request.Filter.DeliveryStatus
: null,
FromDate = request.Filter?.FromDate?.ToDateTime() ?? request.Filter?.PaymentDate?.ToDateTime(),
ToDate = request.Filter?.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllUserOrderByFilterResponse
{
MetaData = result.MetaData.Adapt<MetaData>()
};
foreach (var model in result.Models)
{
var orderModel = new GetAllUserOrderByFilterResponseModel
{
Id = model.Id,
Amount = model.Amount,
PackageId = model.PackageId ?? 0,
TransactionId = model.TransactionId,
UserId = model.UserId,
UserAddressId = model.UserAddressId,
UserAddressText = model.UserAddressText,
TrackingCode = model.TrackingCode,
DeliveryDescription = model.DeliveryDescription,
UserFullName = model.UserFullName,
UserNationalCode = model.UserNationalCode,
VatAmount = model.VatAmount,
VatPercentage = model.VatPercentage
};
orderModel.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)model.PaymentStatus;
if (model.PaymentDate.HasValue)
orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc));
if (model.PaymentMethod.HasValue)
orderModel.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)model.PaymentMethod.Value;
orderModel.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)model.DeliveryStatus;
foreach (var fd in model.FactorDetails)
{
orderModel.FactorDetails.Add(new GetAllUserOrderByFilterResponseModelFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.ProductTitle,
ProductThumbnailPath = fd.ProductThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
});
}
response.Models.Add(orderModel);
}
return response;
}
public override async Task<SubmitShopBuyOrderResponse> SubmitShopBuyOrder(SubmitShopBuyOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<SubmitShopBuyOrderRequest, SubmitShopBuyOrderCommand, SubmitShopBuyOrderResponse>(request, context);
// Get UserId from JWT or request
var userId = !string.IsNullOrEmpty(_currentUserService.UserId)
? long.Parse(_currentUserService.UserId)
: request.UserId;
if (userId == 0)
{
throw new RpcException(new Status(StatusCode.Unauthenticated, "User not authenticated"));
}
// Get user's cart items with product details
var cartItems = await _context.UserCarts
.Include(c => c.Product)
.Where(c => c.UserId == userId && !c.IsDeleted)
.ToListAsync(context.CancellationToken);
if (!cartItems.Any())
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, "سبد خرید خالی است"));
}
// Get user's default address
var defaultAddress = await _context.UserAddresses
.Where(a => a.UserId == userId && a.IsDefault && !a.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (defaultAddress == null)
{
// Check if user has any address at all
var hasAnyAddress = await _context.UserAddresses
.AnyAsync(a => a.UserId == userId && !a.IsDeleted, context.CancellationToken);
throw new RpcException(new Status(StatusCode.FailedPrecondition,
hasAnyAddress
? "لطفاً یک آدرس را به عنوان پیش‌فرض انتخاب کنید."
: "آدرسی ثبت نشده است. لطفاً ابتدا یک آدرس اضافه کنید."));
}
// Calculate amounts
const decimal vatRate = 0.09m;
long baseAmount = cartItems.Sum(c => c.Product.Price * c.Count);
long vatAmount = (long)(baseAmount * vatRate);
long totalAmount = baseAmount + vatAmount;
// Validate total amount
if (request.TotalAmount > 0 && Math.Abs(totalAmount - request.TotalAmount) > 100)
{
throw new RpcException(new Status(StatusCode.InvalidArgument,
$"مبلغ نامعتبر است. محاسبه شده: {totalAmount}, دریافتی: {request.TotalAmount}"));
}
// Get user's wallet
var wallet = await _context.UserWallets
.Where(w => w.UserId == userId)
.FirstOrDefaultAsync(context.CancellationToken);
if (wallet == null)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, "کیف پول یافت نشد"));
}
// Check wallet balance
if (wallet.Balance < totalAmount)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition,
$"موجودی کیف پول کافی نیست. موجودی: {wallet.Balance:N0} تومان، مورد نیاز: {totalAmount:N0} تومان"));
}
// Create transaction
var transaction = new Transaction
{
Amount = totalAmount,
Description = $"خرید محصولات - سفارش شماره در حال ایجاد",
PaymentStatus = CMSMicroservice.Domain.Enums.PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
Type = CMSMicroservice.Domain.Enums.TransactionType.Buy,
RefId = $"SHOP_{DateTime.UtcNow.Ticks}"
};
_context.Transactions.Add(transaction);
await _context.SaveChangesAsync(context.CancellationToken);
// Deduct from wallet
var oldBalance = wallet.Balance;
wallet.Balance -= totalAmount;
// Create wallet change log
var walletLog = new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = -totalAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = 0,
IsIncrease = false,
RefrenceId = transaction.Id
};
_context.UserWalletChangeLogs.Add(walletLog);
// Create order
var order = new UserOrder
{
UserId = userId,
UserAddressId = defaultAddress.Id,
Amount = totalAmount,
TransactionId = transaction.Id,
PaymentStatus = CMSMicroservice.Domain.Enums.PaymentStatus.Success,
PaymentDate = DateTime.UtcNow,
PaymentMethod = CMSMicroservice.Domain.Enums.PaymentMethod.Wallet,
DeliveryStatus = CMSMicroservice.Domain.Enums.DeliveryStatus.Pending,
HasVAT = true
};
_context.UserOrders.Add(order);
await _context.SaveChangesAsync(context.CancellationToken);
// Update transaction description with order ID
transaction.Description = $"خرید محصولات - سفارش #{order.Id}";
await _context.SaveChangesAsync(context.CancellationToken);
// Create order VAT record
var orderVat = new OrderVAT
{
OrderId = order.Id,
VATRate = vatRate,
BaseAmount = baseAmount,
VATAmount = vatAmount,
TotalAmount = totalAmount
};
_context.OrderVATs.Add(orderVat);
// Create factor details for each cart item
foreach (var cartItem in cartItems)
{
var factorDetail = new FactorDetails
{
OrderId = order.Id,
ProductId = cartItem.ProductId,
Count = cartItem.Count,
UnitPrice = cartItem.Product.Price,
UnitDiscount = 0,
UnitDiscountPrice = cartItem.Product.Price,
IsChangePrice = false
};
_context.FactorDetails.Add(factorDetail);
}
await _context.SaveChangesAsync(context.CancellationToken);
// Clear user's cart
foreach (var cartItem in cartItems)
{
cartItem.IsDeleted = true;
}
await _context.SaveChangesAsync(context.CancellationToken);
return new SubmitShopBuyOrderResponse
{
Id = order.Id
};
}
[RequiresPermission(PermissionNames.OrdersCancel)]
public override async Task<CancelOrderResponse> CancelOrder(CancelOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CancelOrderRequest, CancelOrderCommand, CancelOrderResponse>(request, context);
var command = new CancelOrderByAdminCommand
{
OrderId = request.OrderId,
CancelReason = request.CancelReason,
RefundToWallet = request.RefundPayment
};
await _sender.Send(command, context.CancellationToken);
return new CancelOrderResponse
{
OrderId = request.OrderId,
Status = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)(int)Domain.Enums.DeliveryStatus.Cancelled,
Message = "سفارش با موفقیت لغو شد",
RefundProcessed = request.RefundPayment
};
}
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<UpdateOrderStatusResponse> UpdateOrderStatus(UpdateOrderStatusRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateOrderStatusRequest, UpdateOrderStatusCommand, UpdateOrderStatusResponse>(request, context);
var order = await _context.UserOrders.FindAsync(new object[] { request.OrderId }, context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var oldStatus = (int)order.DeliveryStatus;
var command = new Application.OrderManagementCQ.Commands.UpdateOrderStatus.UpdateOrderStatusCommand
{
OrderId = request.OrderId,
NewStatus = (Domain.Enums.DeliveryStatus)request.NewStatus
};
await _sender.Send(command, context.CancellationToken);
return new UpdateOrderStatusResponse
{
Success = true,
Message = "وضعیت سفارش با موفقیت تغییر کرد",
OldStatus = oldStatus,
NewStatus = request.NewStatus
};
}
[RequiresPermission(PermissionNames.ReportsView)]
public override async Task<GetOrdersByDateRangeResponse> GetOrdersByDateRange(GetOrdersByDateRangeRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetOrdersByDateRangeRequest, GetOrdersByDateRangeQuery, GetOrdersByDateRangeResponse>(request, context);
var pageNumber = request.PageNumber > 0 ? request.PageNumber : 1;
var pageSize = request.PageSize > 0 ? request.PageSize : 20;
var query = _context.UserOrders
.Include(o => o.User)
.Include(o => o.FactorDetails)
.Where(o => !o.IsDeleted);
if (request.StartDate != null)
query = query.Where(o => o.Created >= request.StartDate.ToDateTime());
if (request.EndDate != null)
query = query.Where(o => o.Created <= request.EndDate.ToDateTime());
if (request.Status != null)
query = query.Where(o => (int)o.DeliveryStatus == request.Status.Value);
if (request.UserId != null)
query = query.Where(o => o.UserId == request.UserId.Value);
var totalCount = await query.CountAsync(context.CancellationToken);
var orders = await query
.OrderByDescending(o => o.Created)
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.Select(o => new OrderSummaryDto
{
OrderId = o.Id,
OrderNumber = $"ORD-{o.Id:D6}",
UserId = o.UserId,
UserFullName = o.User != null ? (o.User.FirstName + " " + o.User.LastName) : string.Empty,
TotalAmount = o.Amount,
Status = (int)o.DeliveryStatus,
StatusName = o.DeliveryStatus.ToString(),
ItemsCount = o.FactorDetails.Count,
CreatedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(o.Created, DateTimeKind.Utc))
})
.ToListAsync(context.CancellationToken);
return new GetOrdersByDateRangeResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = pageNumber,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
HasPrevious = pageNumber > 1,
HasNext = pageNumber * pageSize < totalCount
},
Orders = { orders }
};
}
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<ApplyDiscountToOrderResponse> ApplyDiscountToOrder(ApplyDiscountToOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ApplyDiscountToOrderRequest, ApplyDiscountToOrderCommand, ApplyDiscountToOrderResponse>(request, context);
var order = await _context.UserOrders.FindAsync(new object[] { request.OrderId }, context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
if (order.PaymentStatus == Domain.Enums.PaymentStatus.Success)
return new ApplyDiscountToOrderResponse
{
Success = false,
Message = "امکان اعمال تخفیف برای سفارش پرداخت شده وجود ندارد"
};
var originalAmount = order.Amount;
var discountAmount = Math.Min(request.DiscountAmount, originalAmount);
order.Amount = originalAmount - discountAmount;
await _context.SaveChangesAsync(context.CancellationToken);
return new ApplyDiscountToOrderResponse
{
Success = true,
Message = $"تخفیف {discountAmount:N0} تومان با موفقیت اعمال شد",
OriginalAmount = originalAmount,
DiscountAmount = discountAmount,
FinalAmount = order.Amount
};
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<CalculateOrderPVResponse> CalculateOrderPV(CalculateOrderPVRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CalculateOrderPVRequest, CalculateOrderPVQuery, CalculateOrderPVResponse>(request, context);
var order = await _context.UserOrders
.Include(o => o.FactorDetails)
.ThenInclude(fd => fd.Product)
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var products = new List<ProductPVDto>();
long totalPV = 0;
foreach (var fd in order.FactorDetails.Where(f => !f.IsDeleted))
{
// PV = UnitPrice * Count (simplified - can be customized)
long unitPV = fd.UnitPrice;
long itemPV = unitPV * fd.Count;
totalPV += itemPV;
products.Add(new ProductPVDto
{
ProductId = fd.ProductId,
ProductTitle = fd.Product?.Title ?? string.Empty,
Quantity = fd.Count,
UnitPv = unitPV,
TotalPv = itemPV
});
}
return new CalculateOrderPVResponse
{
OrderId = request.OrderId,
TotalPv = totalPV,
Products = { products }
};
}
// ============= Customer-specific Methods =============
public override async Task<CreateNewUserOrderResponse> CreateNewOrderForCustomer(CreateNewUserOrderRequest request, ServerCallContext context)
{
// For now, return empty response - will be implemented properly later
return new CreateNewUserOrderResponse();
}
public override async Task<SubmitShopBuyOrderResponse> SubmitOrderForCustomer(SubmitShopBuyOrderRequest request, ServerCallContext context)
{
// For now, return empty response - will be implemented properly later
return new SubmitShopBuyOrderResponse();
}
public override async Task<GetAllUserOrderByFilterResponse> GetCustomerOrders(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
// Customer endpoint: ALWAYS resolve userId from JWT (customer can only see own orders)
var customerUserId = long.TryParse(_currentUserService.UserId, out var uid) ? uid : 0;
if (customerUserId == 0)
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
var query = new GetCustomerOrdersQuery
{
UserId = customerUserId,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
PaymentStatusFilter = request.Filter?.HasPaymentStatus == true
? (int?)request.Filter.PaymentStatus
: null,
DeliveryStatusFilter = request.Filter?.HasDeliveryStatus == true
? (int?)request.Filter.DeliveryStatus
: null,
FromDate = request.Filter?.FromDate?.ToDateTime() ?? request.Filter?.PaymentDate?.ToDateTime(),
ToDate = request.Filter?.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetAllUserOrderByFilterResponse
{
MetaData = result.MetaData.Adapt<MetaData>()
};
foreach (var model in result.Models)
{
var orderModel = new GetAllUserOrderByFilterResponseModel
{
Id = model.Id,
Amount = model.Amount,
PackageId = model.PackageId ?? 0,
TransactionId = model.TransactionId,
UserId = model.UserId,
UserAddressId = model.UserAddressId,
UserAddressText = model.UserAddressText,
TrackingCode = model.TrackingCode,
DeliveryDescription = model.DeliveryDescription,
UserFullName = model.UserFullName,
UserNationalCode = model.UserNationalCode,
VatAmount = model.VatAmount,
VatPercentage = model.VatPercentage
};
orderModel.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)model.PaymentStatus;
if (model.PaymentDate.HasValue)
orderModel.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(model.PaymentDate.Value, DateTimeKind.Utc));
if (model.PaymentMethod.HasValue)
orderModel.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)model.PaymentMethod.Value;
orderModel.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)model.DeliveryStatus;
foreach (var fd in model.FactorDetails)
{
orderModel.FactorDetails.Add(new GetAllUserOrderByFilterResponseModelFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.ProductTitle,
ProductThumbnailPath = fd.ProductThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
});
}
response.Models.Add(orderModel);
}
return response;
}
public override async Task<GetUserOrderResponse> GetCustomerOrder(GetUserOrderRequest request, ServerCallContext context)
{
// Customer endpoint: ALWAYS resolve userId from JWT (customer can only see own orders)
var customerUserId = long.TryParse(_currentUserService.UserId, out var uid) ? uid : 0;
if (customerUserId == 0)
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
var query = new GetCustomerOrderQuery
{
OrderId = request.Id,
UserId = customerUserId
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetUserOrderResponse
{
Id = result.Id,
Amount = result.Amount,
PackageId = result.PackageId ?? 0,
TransactionId = result.TransactionId,
UserId = result.UserId,
UserAddressId = result.UserAddressId,
UserAddressText = result.UserAddressText,
TrackingCode = result.TrackingCode,
DeliveryDescription = result.DeliveryDescription,
UserFullName = result.UserFullName,
UserNationalCode = result.UserNationalCode
};
// VAT Info
if (result.VatAmount > 0)
{
response.VatInfo = new OrderVATInfo
{
VatRate = result.VatPercentage / 100,
BaseAmount = result.Amount - result.VatAmount,
VatAmount = result.VatAmount,
TotalAmount = result.Amount,
IsPaid = result.PaymentStatus == Domain.Enums.PaymentStatus.Success
};
}
response.PaymentStatus = (CMSMicroservice.Protobuf.Protos.PaymentStatus)result.PaymentStatus;
if (result.PaymentDate.HasValue)
response.PaymentDate = Timestamp.FromDateTime(DateTime.SpecifyKind(result.PaymentDate.Value, DateTimeKind.Utc));
if (result.PaymentMethod.HasValue)
response.PaymentMethod = (CMSMicroservice.Protobuf.Protos.PaymentMethod)result.PaymentMethod.Value;
response.DeliveryStatus = (CMSMicroservice.Protobuf.Protos.DeliveryStatus)result.DeliveryStatus;
foreach (var fd in result.FactorDetails)
{
response.FactorDetails.Add(new GetUserOrderResponseFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.ProductTitle,
ProductThumbnailPath = fd.ProductThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
});
}
return response;
}
// ============= Customer-specific Method Implementations =============
public override async Task<CustomerCancelOrderResponse> CustomerCancelOrder(CustomerCancelOrderRequest request, ServerCallContext context)
{
var order = await _context.UserOrders
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (order == null)
return new CustomerCancelOrderResponse { Success = false, Message = "سفارش یافت نشد" };
if (order.DeliveryStatus != Domain.Enums.DeliveryStatus.Pending)
return new CustomerCancelOrderResponse { Success = false, Message = "فقط سفارش‌های در انتظار قابل لغو هستند" };
var refundAmount = order.Amount;
order.DeliveryStatus = Domain.Enums.DeliveryStatus.Cancelled;
// Refund to wallet if payment was from wallet
if (order.PaymentStatus == Domain.Enums.PaymentStatus.Success && order.PaymentMethod == Domain.Enums.PaymentMethod.Wallet)
{
var wallet = await _context.UserWallets
.Where(w => w.UserId == order.UserId)
.FirstOrDefaultAsync(context.CancellationToken);
if (wallet != null)
{
wallet.Balance += refundAmount;
_context.UserWalletChangeLogs.Add(new UserWalletChangeLog
{
WalletId = wallet.Id,
CurrentBalance = wallet.Balance,
ChangeValue = refundAmount,
CurrentNetworkBalance = wallet.NetworkBalance,
ChangeNerworkValue = 0,
CurrentDiscountBalance = wallet.DiscountBalance,
ChangeDiscountValue = 0,
IsIncrease = true,
RefrenceId = order.TransactionId ?? 0
});
}
}
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerCancelOrderResponse
{
Success = true,
Message = "سفارش شما با موفقیت لغو شد",
RefundAmount = refundAmount,
RefundTransactionId = $"REF{order.Id}"
};
}
public override async Task<GetCustomerOrderHistoryResponse> GetCustomerOrderHistory(GetCustomerOrderHistoryRequest request, ServerCallContext context)
{
var query = new GetCustomerOrderHistoryQuery
{
UserId = request.UserId,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
StatusFilter = request.StatusFilter != OrderStatusEnum.OrderStatusPending
? (int?)request.StatusFilter
: null,
FromDate = request.FromDate?.ToDateTime(),
ToDate = request.ToDate?.ToDateTime()
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerOrderHistoryResponse
{
MetaData = result.MetaData.Adapt<MetaData>()
};
foreach (var order in result.Orders)
{
response.Orders.Add(new Protobuf.Protos.UserOrder.CustomerOrderModel
{
Id = order.Id,
Amount = order.Amount,
PackageId = order.PackageId ?? 0,
PackageName = order.PackageName,
Status = (OrderStatusEnum)order.Status,
StatusMessage = order.StatusMessage,
OrderDate = Timestamp.FromDateTime(DateTime.SpecifyKind(order.OrderDate, DateTimeKind.Utc)),
DeliveryDate = order.DeliveryDate.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(order.DeliveryDate.Value, DateTimeKind.Utc))
: null,
TrackingCode = order.TrackingCode,
ItemsCount = order.ItemsCount,
CanCancel = order.CanCancel,
CanReorder = order.CanReorder
});
}
return response;
}
public override async Task<CustomerTrackOrderResponse> CustomerTrackOrder(CustomerTrackOrderRequest request, ServerCallContext context)
{
var order = await _context.UserOrders
.Include(o => o.FactorDetails)
.Include(o => o.Package)
.Where(o => o.Id == request.OrderId && !o.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (order == null)
throw new RpcException(new Status(StatusCode.NotFound, "سفارش یافت نشد"));
var orderModel = new Protobuf.Protos.UserOrder.CustomerOrderModel
{
Id = order.Id,
Amount = order.Amount,
PackageId = order.PackageId ?? 0,
PackageName = order.Package?.Title ?? string.Empty,
Status = (OrderStatusEnum)(int)order.DeliveryStatus,
StatusMessage = GetDeliveryStatusPersian(order.DeliveryStatus),
OrderDate = Timestamp.FromDateTime(DateTime.SpecifyKind(order.Created, DateTimeKind.Utc)),
TrackingCode = order.TrackingCode ?? string.Empty,
ItemsCount = order.FactorDetails.Count(f => !f.IsDeleted),
CanCancel = order.DeliveryStatus == Domain.Enums.DeliveryStatus.Pending,
CanReorder = order.DeliveryStatus == Domain.Enums.DeliveryStatus.Delivered
};
var response = new CustomerTrackOrderResponse
{
Order = orderModel,
DeliveryInfo = new DeliveryTrackingInfo
{
TrackingCode = order.TrackingCode ?? string.Empty,
CourierName = "پست پیشتاز",
EstimatedDelivery = order.DeliveryDescription ?? string.Empty,
CurrentLocation = string.Empty
}
};
// Add current status to history
response.StatusHistory.Add(new OrderStatusHistory
{
Status = (OrderStatusEnum)(int)order.DeliveryStatus,
StatusMessage = GetDeliveryStatusPersian(order.DeliveryStatus),
ChangedAt = order.LastModified.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(order.LastModified.Value, DateTimeKind.Utc))
: Timestamp.FromDateTime(DateTime.SpecifyKind(order.Created, DateTimeKind.Utc)),
ChangedBy = "سیستم"
});
return response;
}
public override async Task<CustomerReorderResponse> CustomerReorderPreviousOrder(CustomerReorderRequest request, ServerCallContext context)
{
var originalOrder = await _context.UserOrders
.Include(o => o.FactorDetails)
.ThenInclude(fd => fd.Product)
.Where(o => o.Id == request.OriginalOrderId && !o.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (originalOrder == null)
return new CustomerReorderResponse { Success = false, Message = "سفارش اصلی یافت نشد" };
var userId = originalOrder.UserId;
// Add items from old order to cart
foreach (var fd in originalOrder.FactorDetails.Where(f => !f.IsDeleted))
{
var existingCartItem = await _context.UserCarts
.Where(c => c.UserId == userId && c.ProductId == fd.ProductId && !c.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (existingCartItem != null)
{
existingCartItem.Count += fd.Count;
}
else
{
_context.UserCarts.Add(new UserCart
{
UserId = userId,
ProductId = fd.ProductId,
Count = fd.Count
});
}
}
await _context.SaveChangesAsync(context.CancellationToken);
// Calculate total with current prices
long totalAmount = originalOrder.FactorDetails
.Where(f => !f.IsDeleted)
.Sum(fd => request.UseCurrentPrices && fd.Product != null
? fd.Product.Price * fd.Count
: fd.UnitPrice * fd.Count);
return new CustomerReorderResponse
{
Success = true,
Message = "محصولات به سبد خرید اضافه شدند",
NewOrderId = 0, // Cart items added, no order created yet
TotalAmount = totalAmount
};
}
public override Task<GetVATRateResponse> GetVATRate(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
{
// VAT Rate for Iran: 9% (نرخ مالیات بر ارزش افزوده ایران)
return Task.FromResult(new GetVATRateResponse
{
VatRate = 0.09,
VatPercentage = 9,
IsEnabled = true
});
}
// ============= Helper Methods =============
private static string GetDeliveryStatusPersian(Domain.Enums.DeliveryStatus status) => status switch
{
Domain.Enums.DeliveryStatus.None => "نامشخص",
Domain.Enums.DeliveryStatus.Pending => "در انتظار ارسال",
Domain.Enums.DeliveryStatus.InTransit => "در حال ارسال",
Domain.Enums.DeliveryStatus.Delivered => "تحویل داده شده",
Domain.Enums.DeliveryStatus.Returned => "مرجوع شده",
Domain.Enums.DeliveryStatus.Cancelled => "لغو شده",
_ => status.ToString()
};
}
@@ -1,4 +1,5 @@
using CMSMicroservice.Protobuf.Protos.User;
using CMSMicroservice.Protobuf.Protos.City;
using CMSMicroservice.WebApi.Common.Services;
using CMSMicroservice.Application.UserCQ.Commands.CreateNewUser;
using CMSMicroservice.Application.UserCQ.Commands.UpdateUser;
@@ -9,14 +10,46 @@ using CMSMicroservice.Application.UserCQ.Queries.GetJwtToken;
using CMSMicroservice.Application.UserCQ.Queries.AdminGetJwtToken;
using CMSMicroservice.Application.UserCQ.Commands.SetPasswordForUser;
using CMSMicroservice.Application.UserCQ.Commands.RefreshToken;
using CMSMicroservice.Application.UserCQ.Commands.CreateNewOtpToken;
using CMSMicroservice.Application.UserCQ.Commands.VerifyOtpToken;
using CMSMicroservice.Application.UserCQ.Commands.AcceptContract;
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerProfile;
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerReferrals;
using CMSMicroservice.Application.UserCQ.Queries.GetCustomerSettings;
using CMSMicroservice.Application.Common.Interfaces;
using Google.Protobuf.WellKnownTypes;
using Microsoft.EntityFrameworkCore;
using Grpc.Core;
using System.Collections.Generic;
using System.Linq;
using MediatR;
using Mapster;
using AppModels = CMSMicroservice.Application.Common.Models;
namespace CMSMicroservice.WebApi.Services;
public class UserService : UserContract.UserContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
private readonly IHashService _hashService;
private readonly CMSMicroservice.Application.Common.FileManager.IFileManager _fileManager;
public UserService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public UserService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IHashService hashService,
CMSMicroservice.Application.Common.FileManager.IFileManager fileManager)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_hashService = hashService;
_fileManager = fileManager;
}
public override async Task<CreateNewUserResponse> CreateNewUser(CreateNewUserRequest request, ServerCallContext context)
{
@@ -32,6 +65,10 @@ public class UserService : UserContract.UserContractBase
}
public override async Task<GetUserResponse> GetUser(GetUserRequest request, ServerCallContext context)
{
// اگر Id ارسال نشده، از JWT بخون (برای کلاینت مشتری)
if (request.Id == 0)
request.Id = GetCurrentUserId();
return await _dispatchRequestToCQRS.Handle<GetUserRequest, GetUserQuery, GetUserResponse>(request, context);
}
public override async Task<GetAllUserByFilterResponse> GetAllUserByFilter(GetAllUserByFilterRequest request, ServerCallContext context)
@@ -54,4 +91,330 @@ public class UserService : UserContract.UserContractBase
{
return await _dispatchRequestToCQRS.Handle<RefreshTokenRequest, RefreshTokenCommand, RefreshTokenResponse>(request, context);
}
// ============= Customer-specific Methods =============
public override async Task<CreateNewOtpTokenResponse> CreateNewOtpToken(CreateNewOtpTokenRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewOtpTokenRequest, CreateNewOtpTokenCommand, CreateNewOtpTokenResponse>(request, context);
}
public override async Task<VerifyOtpTokenResponse> VerifyOtpToken(VerifyOtpTokenRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<VerifyOtpTokenRequest, VerifyOtpTokenCommand, VerifyOtpTokenResponse>(request, context);
}
public override async Task<AcceptContractResponse> AcceptContract(AcceptContractRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<AcceptContractRequest, AcceptContractCommand, AcceptContractResponse>(request, context);
}
public override async Task<GetUserForCustomerResponse> GetUserForCustomer(GetUserForCustomerRequest request, ServerCallContext context)
{
var userId = GetCurrentUserId();
var user = await _context.Users
.AsNoTracking()
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (user == null)
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
return new GetUserForCustomerResponse
{
Id = user.Id,
FirstName = user.FirstName ?? string.Empty,
LastName = user.LastName ?? string.Empty,
Mobile = user.Mobile,
Email = user.Email ?? string.Empty,
NationalCode = user.NationalCode ?? string.Empty,
AvatarPath = user.AvatarPath ?? string.Empty,
ParentId = user.NetworkParentId,
ReferralCode = user.ReferralCode ?? string.Empty,
IsMobileVerified = user.IsMobileVerified,
MobileVerifiedAt = user.MobileVerifiedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(user.MobileVerifiedAt.Value, DateTimeKind.Utc))
: null,
EmailNotifications = user.EmailNotifications,
SmsNotifications = user.SmsNotifications,
PushNotifications = user.PushNotifications,
BirthDate = user.BirthDate.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(user.BirthDate.Value, DateTimeKind.Utc))
: null
};
}
public override async Task<Empty> UpdateCustomerProfile(UpdateCustomerProfileRequest request, ServerCallContext context)
{
var userId = GetCurrentUserId();
var user = await _context.Users
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (user == null)
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
if (request.FirstName != null) user.FirstName = request.FirstName;
if (request.LastName != null) user.LastName = request.LastName;
if (request.Email != null) user.Email = request.Email;
if (request.NationalCode != null) user.NationalCode = request.NationalCode;
if (request.BirthDate != null) user.BirthDate = request.BirthDate.ToDateTime();
await _context.SaveChangesAsync(context.CancellationToken);
return new Empty();
}
public override async Task<GetCustomerProfileResponse> GetCustomerProfile(GetCustomerProfileRequest request, ServerCallContext context)
{
var query = new GetCustomerProfileQuery { UserId = 0 };
var result = await _sender.Send(query, context.CancellationToken);
return new GetCustomerProfileResponse
{
Id = result.Id,
FirstName = result.FirstName,
LastName = result.LastName,
Mobile = result.Mobile,
Email = result.Email,
NationalCode = result.NationalCode,
AvatarPath = result.AvatarPath,
ParentId = result.ParentId,
ReferralCode = result.ReferralCode,
IsMobileVerified = result.IsMobileVerified,
MobileVerifiedAt = result.MobileVerifiedAt.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(result.MobileVerifiedAt.Value, DateTimeKind.Utc))
: null,
EmailNotifications = result.EmailNotifications,
SmsNotifications = result.SmsNotifications,
PushNotifications = result.PushNotifications,
BirthDate = result.BirthDate.HasValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(result.BirthDate.Value, DateTimeKind.Utc))
: null,
FullName = result.FullName,
ProfileCompletionPercentage = result.ProfileCompletionPercentage
};
}
public override async Task<ChangeCustomerPasswordResponse> ChangeCustomerPassword(ChangeCustomerPasswordRequest request, ServerCallContext context)
{
if (request.NewPassword != request.ConfirmPassword)
{
return new ChangeCustomerPasswordResponse
{
Success = false,
Message = "رمز عبور جدید و تکرار آن یکسان نیستند"
};
}
if (request.NewPassword.Length < 6)
{
return new ChangeCustomerPasswordResponse
{
Success = false,
Message = "رمز عبور باید حداقل 6 کاراکتر باشد"
};
}
var userId = GetCurrentUserId();
var user = await _context.Users
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (user == null)
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
// Verify current password
if (!string.IsNullOrEmpty(user.HashPassword))
{
if (!_hashService.VerifyPassword(request.CurrentPassword, user.HashPassword))
{
return new ChangeCustomerPasswordResponse
{
Success = false,
Message = "رمز عبور فعلی نادرست است"
};
}
}
// Hash and save new password
user.HashPassword = _hashService.HashPassword(request.NewPassword);
await _context.SaveChangesAsync(context.CancellationToken);
return new ChangeCustomerPasswordResponse
{
Success = true,
Message = "رمز عبور با موفقیت تغییر یافت"
};
}
public override async Task<GetCustomerReferralsResponse> GetCustomerReferrals(GetCustomerReferralsRequest request, ServerCallContext context)
{
var query = new GetCustomerReferralsQuery
{
UserId = 0,
PaginationState = request.PaginationState?.Adapt<AppModels.PaginationState>(),
StatusFilter = request.StatusFilter
};
var result = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerReferralsResponse
{
MetaData = new MetaData
{
CurrentPage = result.MetaData.CurrentPage,
TotalPage = result.MetaData.TotalPage,
PageSize = result.MetaData.PageSize,
TotalCount = result.MetaData.TotalCount,
HasPrevious = result.MetaData.HasPrevious,
HasNext = result.MetaData.HasNext
},
Stats = new CMSMicroservice.Protobuf.Protos.User.CustomerReferralStats
{
TotalReferrals = result.Stats.TotalReferrals,
ActiveReferrals = result.Stats.ActiveReferrals,
TotalCommissionEarned = result.Stats.TotalCommissionEarned,
ThisMonthCommission = result.Stats.ThisMonthCommission
}
};
foreach (var referral in result.Referrals)
{
response.Referrals.Add(new CMSMicroservice.Protobuf.Protos.User.CustomerReferralModel
{
Id = referral.Id,
FirstName = referral.FirstName,
LastName = referral.LastName,
Mobile = referral.Mobile,
JoinDate = Timestamp.FromDateTime(DateTime.SpecifyKind(referral.JoinDate, DateTimeKind.Utc)),
IsActive = referral.IsActive,
StatusMessage = referral.StatusMessage,
Level = referral.Level,
TotalCommission = referral.TotalCommission
});
}
return response;
}
public override async Task<UploadCustomerAvatarResponse> UploadCustomerAvatar(UploadCustomerAvatarRequest request, ServerCallContext context)
{
if (request.FileData == null || request.FileData.Length == 0)
{
return new UploadCustomerAvatarResponse
{
Success = false,
Message = "فایل انتخاب نشده است"
};
}
if (request.FileData.Length > 5 * 1024 * 1024) // 5MB limit
{
return new UploadCustomerAvatarResponse
{
Success = false,
Message = "حجم فایل نباید بیش از 5 مگابایت باشد"
};
}
var allowedTypes = new[] { "image/jpeg", "image/jpg", "image/png", "image/gif" };
if (!allowedTypes.Contains(request.FileMimeType?.ToLower()))
{
return new UploadCustomerAvatarResponse
{
Success = false,
Message = "فرمت فایل مجاز نیست. فقط JPG, PNG و GIF مجاز هستند"
};
}
var userId = GetCurrentUserId();
var user = await _context.Users
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (user == null)
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
// Upload to local file manager
var fileBytes = request.FileData.ToByteArray();
var fileName = $"avatar_{userId}_{DateTime.UtcNow.Ticks}";
var mime = request.FileMimeType ?? "image/jpeg";
try
{
var result = await _fileManager.UploadImageAsync(
"Avatars", fileBytes, mime, fileName, context.CancellationToken);
// Update user avatar path in DB
user.AvatarPath = result.Main.Path;
await _context.SaveChangesAsync(context.CancellationToken);
return new UploadCustomerAvatarResponse
{
Success = true,
Message = "تصویر پروفایل با موفقیت آپلود شد",
AvatarUrl = result.Main.Path
};
}
catch (Exception ex)
{
return new UploadCustomerAvatarResponse
{
Success = false,
Message = "خطا در آپلود فایل. لطفاً مجدد تلاش کنید"
};
}
}
public override async Task<GetCustomerSettingsResponse> GetCustomerSettings(GetCustomerSettingsRequest request, ServerCallContext context)
{
var query = new GetCustomerSettingsQuery { UserId = 0 };
var result = await _sender.Send(query, context.CancellationToken);
return new GetCustomerSettingsResponse
{
EmailNotifications = result.EmailNotifications,
SmsNotifications = result.SmsNotifications,
PushNotifications = result.PushNotifications,
MarketingNotifications = result.MarketingNotifications,
PreferredLanguage = result.PreferredLanguage,
TimeZone = result.TimeZone,
TwoFactorAuthEnabled = result.TwoFactorAuthEnabled
};
}
public override async Task<Empty> UpdateCustomerSettings(UpdateCustomerSettingsRequest request, ServerCallContext context)
{
var userId = GetCurrentUserId();
var user = await _context.Users
.Where(u => u.Id == userId && !u.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (user == null)
throw new RpcException(new Status(StatusCode.NotFound, "کاربر یافت نشد"));
user.EmailNotifications = request.EmailNotifications;
user.SmsNotifications = request.SmsNotifications;
user.PushNotifications = request.PushNotifications;
// MarketingNotifications, PreferredLanguage, TimeZone, TwoFactorAuth
// are stored at application level if needed in future
await _context.SaveChangesAsync(context.CancellationToken);
return new Empty();
}
// ============= Helper Methods =============
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
return userId;
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
}
}
@@ -5,14 +5,32 @@ using CMSMicroservice.Application.UserWalletCQ.Commands.UpdateUserWallet;
using CMSMicroservice.Application.UserWalletCQ.Commands.DeleteUserWallet;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetUserWallet;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetAllUserWalletByFilter;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWalletChangeLog;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawals;
using CMSMicroservice.Application.UserWalletCQ.Queries.GetCustomerWithdrawalSettings;
using CMSMicroservice.Application.Common.Interfaces;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
public class UserWalletService : UserWalletContract.UserWalletContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
private readonly ICurrentUserService _currentUserService;
public UserWalletService(IDispatchRequestToCQRS dispatchRequestToCQRS)
public UserWalletService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
}
public override async Task<CreateNewUserWalletResponse> CreateNewUserWallet(CreateNewUserWalletRequest request, ServerCallContext context)
{
@@ -34,4 +52,150 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
{
return await _dispatchRequestToCQRS.Handle<GetAllUserWalletByFilterRequest, GetAllUserWalletByFilterQuery, GetAllUserWalletByFilterResponse>(request, context);
}
// ============= Customer-specific Methods =============
public override async Task<GetCustomerWalletResponse> GetCustomerWallet(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
{
// Customer endpoint: resolve userId from JWT
if (!long.TryParse(_currentUserService.UserId, out var userId) || userId <= 0)
throw new RpcException(new Status(StatusCode.Unauthenticated, "کاربر احراز هویت نشده است"));
var walletQuery = new GetUserWalletQuery { Id = userId };
var wallet = await _sender.Send(walletQuery, context.CancellationToken);
return new GetCustomerWalletResponse
{
Balance = wallet.Balance,
NetworkBalance = wallet.NetworkBalance,
DiscountBalance = wallet.DiscountBalance
};
}
public override async Task<GetCustomerWalletChangeLogResponse> GetCustomerWalletChangeLog(GetCustomerWalletChangeLogRequest request, ServerCallContext context)
{
var query = new GetCustomerWalletChangeLogQuery
{
ReferenceId = request.ReferenceId,
IsIncrease = request.IsIncrease
};
var changeLogs = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerWalletChangeLogResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = 1,
TotalPage = 1,
PageSize = changeLogs.Count,
TotalCount = changeLogs.Count,
HasPrevious = false,
HasNext = false
}
};
foreach (var log in changeLogs)
{
response.Models.Add(new CustomerWalletChangeLogModel
{
CurrentBalance = log.CurrentBalance,
ChangeValue = log.ChangeValue,
CurrentNetworkBalance = log.CurrentNetworkBalance,
ChangeNerworkValue = log.ChangeNerworkValue,
CurrentDiscountBalance = log.CurrentDiscountBalance,
ChangeDiscountValue = log.ChangeDiscountValue,
IsIncrease = log.IsIncrease,
RefrenceId = log.RefrenceId,
CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.SpecifyKind(log.Created, DateTimeKind.Utc))
});
}
return response;
}
public override async Task<Google.Protobuf.WellKnownTypes.Empty> CustomerWithdrawBalance(CustomerWithdrawBalanceRequest request, ServerCallContext context)
{
var userId = GetCurrentUserId();
// Find the commission payout record
var payout = await _context.UserCommissionPayouts
.Where(p => p.Id == request.PayoutId && p.UserId == userId && !p.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
if (payout == null)
throw new RpcException(new Status(StatusCode.NotFound, "رکورد پرداخت کمیسیون یافت نشد"));
// Validate status - can only withdraw if already paid to wallet
if (payout.Status != Domain.Enums.CommissionPayoutStatus.Paid)
throw new RpcException(new Status(StatusCode.FailedPrecondition,
"فقط کمیسیون‌های واریز شده به کیف پول قابل برداشت هستند"));
// Update payout with withdrawal info
payout.WithdrawalMethod = (Domain.Enums.WithdrawalMethod)request.WithdrawalMethod;
payout.IbanNumber = request.IbanNumber;
payout.Status = Domain.Enums.CommissionPayoutStatus.WithdrawRequested;
await _context.SaveChangesAsync(context.CancellationToken);
return new Google.Protobuf.WellKnownTypes.Empty();
}
private long GetCurrentUserId()
{
if (long.TryParse(_currentUserService.UserId, out var userId) && userId > 0)
return userId;
throw new RpcException(new Status(StatusCode.Unauthenticated, "لطفاً وارد حساب کاربری خود شوید"));
}
public override async Task<GetCustomerWithdrawalsResponse> GetCustomerWithdrawals(GetCustomerWithdrawalsRequest request, ServerCallContext context)
{
var query = new GetCustomerWithdrawalsQuery
{
Status = request.Status
};
var withdrawals = await _sender.Send(query, context.CancellationToken);
var response = new GetCustomerWithdrawalsResponse
{
MetaData = new CMSMicroservice.Protobuf.Protos.MetaData
{
CurrentPage = 1,
TotalPage = 1,
PageSize = withdrawals.Count,
TotalCount = withdrawals.Count,
HasPrevious = false,
HasNext = false
}
};
foreach (var withdrawal in withdrawals)
{
response.Models.Add(new CustomerWithdrawalModel
{
Id = withdrawal.Id,
WeekDefinitionId = withdrawal.WeekDefinitionId,
WeekDisplayName = withdrawal.WeekDisplayName,
TotalAmount = withdrawal.TotalAmount,
Status = withdrawal.Status,
WithdrawalMethod = withdrawal.WithdrawalMethod,
IbanNumber = withdrawal.IbanNumber,
Created = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime(DateTime.SpecifyKind(withdrawal.Created, DateTimeKind.Utc))
});
}
return response;
}
public override async Task<GetCustomerWithdrawalSettingsResponse> GetCustomerWithdrawalSettings(Google.Protobuf.WellKnownTypes.Empty request, ServerCallContext context)
{
var query = new GetCustomerWithdrawalSettingsQuery();
var settings = await _sender.Send(query, context.CancellationToken);
return new GetCustomerWithdrawalSettingsResponse
{
MinWithdrawalAmount = settings.MinWithdrawalAmount
};
}
}
@@ -1,5 +1,11 @@
{
"UseRealPaymentGateway": false,
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "00000000-0000-0000-0000-000000000000",
"UseSandbox": true
},
"CmsBaseUrl": "http://localhost:32847",
"FrontOfficeBaseUrl": "http://localhost:5268",
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
@@ -0,0 +1,85 @@
{
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "6b098fc8-f490-47a1-aac3-1de1a1b84404",
"UseSandbox": true
},
"FMS": {
"Address": "https://dl.afrino.co"
},
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
"JwtExpiryInDays": 5,
"ConnectionStrings": {
"DefaultConnection": "Data Source=194.5.195.53,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
"providerName": "System.Data.SqlClient"
},
"Otp": {
"Secret": "K2w8k1h1mH2Qz1kqWk0c8kQ2Pq8q9H1eE2nqN1qQ8x7M="
},
"Monitoring": {
"SentryEnabled": false,
"SentryDsn": "",
"SlackEnabled": false,
"SlackWebhookUrl": "",
"EmailAlertsEnabled": false,
"AdminEmails": [
"admin@example.com"
],
"SmsNotificationsEnabled": false,
"SmsApiKey": "",
"SmsGatewayUrl": ""
},
"Email": {
"Enabled": true,
"SmtpHost": "smtp.gmail.com",
"SmtpPort": 587,
"SmtpUsername": "your-email@gmail.com",
"SmtpPassword": "your-app-password",
"FromEmail": "noreply@foursat.com",
"FromName": "FourSat CMS",
"EnableSsl": true
},
"Sms": {
"Enabled": true,
"Provider": "Kavenegar",
"KavenegarApiKey": "497263626F32626A48685A6137524C4F78575A766E4C74694A556B79317648424964655030682B554545413D",
"Sender": "1000001110100"
},
"DayaPayment": {
"BaseUrl": "https://api.daya.ir",
"ApiKey": "YOUR_DAYA_API_KEY"
},
"DayaApi": {
"UseMock": false,
"BaseAddress": "https://Dayadiamond.ir",
"MerchantPermissionKey": "56146364$04sXjethI5WxhItR1Q9xnmFdJzl2BB8Bclsq8dAy7YVSZp3vtt-wP7ivrcCvmKLq",
"CacheDurationMinutes": 20
},
"Chatika": {
"Enabled": true,
"BaseUrl": "https://api.chatika.ir",
"ApiKey": "tIukvL8dnV4cB3yVWcCD9Xyfbj8rBxm5wPt2mLyJCgTsBBoMTWjt6mFEqQwpw-er"
},
"BackgroundJobs": {
"WeeklyCommissionCalculation": {
"Enabled": true,
"CronExpression": "5 0 * * 0"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
},
"Authentication": {
"Authority": "https://ids.domain.com/",
"Audience": "domain_api"
},
"Seq": {
"ServerUrl": "https://seq.afrino.co",
"ApiKey": "oxpvpUzU1pZxMS4s3Fqq"
}
}
+10 -1
View File
@@ -1,5 +1,14 @@
{
"UseRealPaymentGateway": false,
"PaymentProvider": "zarinpal",
"ZarinPal": {
"MerchantId": "6b098fc8-f490-47a1-aac3-1de1a1b84404",
"UseSandbox": true
},
"CmsBaseUrl": "https://cms.kbs1.ir",
"FrontOfficeBaseUrl": "https://foursat.kbs1.ir",
"FMS": {
"Address": "https://dl.afrino.co"
},
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
@@ -0,0 +1,158 @@
/* FourSat CMS Swagger Custom Styles */
/* Header styling */
.swagger-ui .topbar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-bottom: 3px solid #5a67d8;
}
.swagger-ui .topbar .download-url-wrapper {
display: none;
}
/* API sections styling */
.swagger-ui .scheme-container {
background: #f7fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
/* Tag headers (API groups) */
.swagger-ui .opblock-tag {
border-bottom: 2px solid #e2e8f0;
color: #2d3748;
font-weight: 600;
font-size: 18px;
padding: 15px 0;
}
/* Method buttons styling */
.swagger-ui .opblock.opblock-get .opblock-summary-method {
background: #48bb78;
}
.swagger-ui .opblock.opblock-post .opblock-summary-method {
background: #4299e1;
}
.swagger-ui .opblock.opblock-put .opblock-summary-method {
background: #ed8936;
}
.swagger-ui .opblock.opblock-delete .opblock-summary-method {
background: #f56565;
}
/* Response sections */
.swagger-ui .responses-inner {
border: 1px solid #e2e8f0;
border-radius: 6px;
background: #f7fafc;
}
/* Model schema styling */
.swagger-ui .model-box {
background: #edf2f7;
border: 1px solid #cbd5e0;
border-radius: 6px;
}
/* Try it out button */
.swagger-ui .btn.try-out {
background: #667eea;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-weight: 500;
}
.swagger-ui .btn.try-out:hover {
background: #5a67d8;
}
/* Execute button */
.swagger-ui .btn.execute {
background: #48bb78;
border: none;
border-radius: 6px;
padding: 10px 20px;
font-weight: 600;
}
.swagger-ui .btn.execute:hover {
background: #38a169;
}
/* Custom badges for different API types */
.swagger-ui .info .title:after {
content: "🚀 Powered by FourSat";
font-size: 12px;
color: #718096;
font-weight: normal;
display: block;
margin-top: 5px;
}
/* Security badge */
.swagger-ui .auth-btn-wrapper {
display: flex;
justify-content: flex-end;
padding: 10px;
}
.swagger-ui .btn.authorize {
background: #805ad5;
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-weight: 500;
}
.swagger-ui .btn.authorize:hover {
background: #6b46c1;
}
/* Loading states */
.swagger-ui .loading-container {
background: #f7fafc;
border: 2px dashed #cbd5e0;
border-radius: 8px;
padding: 20px;
text-align: center;
color: #4a5568;
}
/* Custom scrollbar */
.swagger-ui ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.swagger-ui ::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.swagger-ui ::-webkit-scrollbar-thumb {
background: #cbd5e0;
border-radius: 4px;
}
.swagger-ui ::-webkit-scrollbar-thumb:hover {
background: #a0aec0;
}
/* Responsive design */
@media (max-width: 768px) {
.swagger-ui .topbar {
padding: 10px;
}
.swagger-ui .info .title {
font-size: 24px;
}
}