feat: Implement file management and authorization features
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 3m9s

- Add RequiresPermissionAttribute for gRPC method access control.
- Create IFileManagementService interface for file upload and management.
- Implement AddProductImageCommand and handler for adding product images.
- Implement CreateNewProductsCommand and handler for creating new products with image uploads.
- Implement DeleteProductsCommand and handler for deleting products and their associations.
- Implement RemoveProductImageCommand and handler for removing product images from galleries.
- Implement UpdateProductsCommand and handler for updating product details and images.
- Create GetProductGalleryQuery and handler for retrieving product galleries.
- Implement PermissionService for role-based access control using JWT claims.
- Implement FileManagementService for handling file uploads and image optimization.
- Define gRPC service and messages for file management in fms.proto.
- Add FluentValidation for request validation in various commands.
- Create PermissionInterceptor for enforcing permissions on gRPC methods.
This commit is contained in:
masoodafar-web
2026-02-10 22:04:54 +03:30
parent f64b6be7da
commit b42d9e141d
65 changed files with 3082 additions and 2384 deletions
@@ -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;
@@ -15,16 +16,19 @@ public class AppVersionService : AppVersionContract.AppVersionContractBase
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
[RequiresPermission(PermissionNames.SettingsView)]
public override async Task<GetAppVersionResponse> GetAppVersion(GetAppVersionRequest request, ServerCallContext context)
{
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);
@@ -100,7 +100,22 @@ public class CategoryService : CategoryContract.CategoryContractBase
public override async Task<GetCategoryByIdForCustomerResponse> GetCategoryByIdForCustomer(GetCategoryByIdForCustomerRequest request, ServerCallContext context)
{
// TODO: Implement using existing CMS Category Application layer
throw new RpcException(new Status(StatusCode.Unimplemented, "GetCategoryByIdForCustomer not implemented yet"));
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(
@@ -28,18 +34,50 @@ public class CityService : CityContract.CityContractBase
public override async Task<GetCitiesForCustomerResponse> GetCitiesForCustomer(
GetCitiesForCustomerRequest request, ServerCallContext context)
{
// TODO: Implement using existing CMS City Application layer
// For now, return empty response
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 = request.PageNumber,
PageSize = request.PageSize,
TotalCount = 0,
TotalPage = 0,
HasNext = false,
HasPrevious = false
CurrentPage = pageNumber,
PageSize = pageSize,
TotalCount = totalCount,
TotalPage = (int)Math.Ceiling(totalCount / (double)pageSize),
HasNext = pageNumber * pageSize < totalCount,
HasPrevious = pageNumber > 1
}
};
}
@@ -47,34 +85,126 @@ public class CityService : CityContract.CityContractBase
public override async Task<GetCityByIdForCustomerResponse> GetCityByIdForCustomer(
GetCityByIdForCustomerRequest request, ServerCallContext context)
{
// TODO: Implement using existing CMS City Application layer
throw new RpcException(new Status(StatusCode.Unimplemented, "GetCityByIdForCustomer not implemented yet"));
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)
{
// TODO: Implement using existing CMS City Application layer
throw new RpcException(new Status(StatusCode.Unimplemented, "GetCitiesByStateForCustomer not implemented yet"));
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 placeholder for future expansion
// Admin Methods
public override async Task<CreateCityResponse> CreateCity(
CreateCityRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "CreateCity not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "UpdateCity not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "DeleteCity not implemented yet"));
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,4 +1,5 @@
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;
@@ -109,6 +110,7 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
/// <summary>
/// دریافت تمام تنظیمات
/// </summary>
[RequiresPermission(PermissionNames.SettingsView)]
public override Task<GetAllConfigurationsResponse> GetAllConfigurations(GetAllConfigurationsRequest request, ServerCallContext context)
{
var response = new GetAllConfigurationsResponse();
@@ -145,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)
@@ -18,9 +18,12 @@ 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;
@@ -28,11 +31,13 @@ public class InventoryService : InventoryContract.InventoryContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly IMediator _mediator;
private readonly IApplicationDbContext _context;
public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, IMediator mediator)
public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, IMediator mediator, IApplicationDbContext context)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_mediator = mediator;
_context = context;
}
// ========== Warehouse Management ==========
@@ -198,28 +203,109 @@ public class InventoryService : InventoryContract.InventoryContractBase
}
}
public override Task<ReserveStockResponse> ReserveStock(ReserveStockRequest request, ServerCallContext context)
public override async Task<ReserveStockResponse> ReserveStock(ReserveStockRequest request, ServerCallContext context)
{
// TODO: Implement with product lookup
throw new RpcException(new Status(StatusCode.Unimplemented, "ReserveStock requires product lookup - not yet implemented"));
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 Task<Empty> ReleaseReservation(ReleaseReservationRequest request, ServerCallContext context)
public override async Task<Empty> ReleaseReservation(ReleaseReservationRequest request, ServerCallContext context)
{
// TODO: Implement with product lookup
throw new RpcException(new Status(StatusCode.Unimplemented, "ReleaseReservation requires product lookup - not yet implemented"));
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 Task<Empty> ConfirmSale(ConfirmSaleRequest request, ServerCallContext context)
public override async Task<Empty> ConfirmSale(ConfirmSaleRequest request, ServerCallContext context)
{
// TODO: Implement with product lookup
throw new RpcException(new Status(StatusCode.Unimplemented, "ConfirmSale requires product lookup - not yet implemented"));
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 Task<ProcessReturnResponse> ProcessReturn(ProcessReturnRequest request, ServerCallContext context)
public override async Task<ProcessReturnResponse> ProcessReturn(ProcessReturnRequest request, ServerCallContext context)
{
// TODO: Implement with product lookup
throw new RpcException(new Status(StatusCode.Unimplemented, "ProcessReturn requires product lookup - not yet implemented"));
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)
@@ -255,10 +341,48 @@ public class InventoryService : InventoryContract.InventoryContractBase
// ========== Bulk Operations ==========
public override Task<BulkAddStockResponse> BulkAddStock(BulkAddStockRequest request, ServerCallContext context)
public override async Task<BulkAddStockResponse> BulkAddStock(BulkAddStockRequest request, ServerCallContext context)
{
// TODO: Implement with product lookup
throw new RpcException(new Status(StatusCode.Unimplemented, "BulkAddStock requires product lookup - not yet implemented"));
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 ==========
@@ -275,28 +399,74 @@ public class InventoryService : InventoryContract.InventoryContractBase
// ========== Reports ==========
public override Task<GetInventorySummaryResponse> GetInventorySummary(GetInventorySummaryRequest request, ServerCallContext context)
public override async Task<GetInventorySummaryResponse> GetInventorySummary(GetInventorySummaryRequest request, ServerCallContext context)
{
// TODO: Implement summary query
return Task.FromResult(new GetInventorySummaryResponse
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 =>
{
TotalProducts = 0,
TotalDiscountProducts = 0,
TotalQuantity = 0,
TotalReserved = 0,
LowStockCount = 0,
OutOfStockCount = 0,
TotalStockValue = 0
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 Task<GetStockValueReportResponse> GetStockValueReport(GetStockValueReportRequest request, ServerCallContext context)
public override async Task<GetStockValueReportResponse> GetStockValueReport(GetStockValueReportRequest request, ServerCallContext context)
{
// TODO: Implement stock value report query
return Task.FromResult(new GetStockValueReportResponse
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 =>
{
TotalValue = 0,
TotalItems = 0
});
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)
@@ -13,11 +13,14 @@ 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 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;
@@ -26,11 +29,22 @@ 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;
public PackageService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
public PackageService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IPaymentGatewayService paymentGateway)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_paymentGateway = paymentGateway;
}
public override async Task<CreateNewPackageResponse> CreateNewPackage(CreateNewPackageRequest request, ServerCallContext context)
{
@@ -142,45 +156,161 @@ public class PackageService : PackageContract.PackageContractBase
public override async Task<CustomerPurchasePackageResponse> CustomerPurchasePackage(CustomerPurchasePackageRequest request, ServerCallContext context)
{
// Mock Customer package purchase with realistic Persian response
var orderId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var authority = "A" + orderId.ToString("D19");
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;
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPurchasePackageResponse
{
Success = true,
Message = "درخواست خرید پکیج با موفقیت ثبت شد",
OrderId = orderId,
PaymentGatewayUrl = $"https://payment.gateway.com/payment?authority={authority}&amount=5600000",
Authority = authority
OrderId = purchase.Id,
PaymentGatewayUrl = paymentResult.GatewayUrl ?? string.Empty,
Authority = paymentResult.RefId ?? string.Empty
};
}
public override async Task<CustomerVerifyPackagePurchaseResponse> CustomerVerifyPackagePurchase(CustomerVerifyPackagePurchaseRequest request, ServerCallContext context)
{
// Mock Customer purchase verification with realistic Persian data
var transactionId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var referenceCode = "REF" + transactionId.ToString();
// Find purchase record
var purchase = await _context.UserPackagePurchases
.Include(p => p.Package)
.Where(p => p.Id == request.OrderId && !p.IsDeleted)
.FirstOrDefaultAsync(context.CancellationToken);
var isSuccessful = request.Status == "OK";
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);
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 = isSuccessful,
Message = isSuccessful ? "خرید پکیج با موفقیت تایید شد" : "خرید پکیج ناموفق بود",
TransactionId = transactionId,
ReferenceCode = referenceCode,
PurchaseInfo = isSuccessful ? new PackagePurchaseInfo
Success = verifyResult.IsSuccess,
Message = verifyResult.IsSuccess ? "خرید پکیج با موفقیت تایید شد" : (verifyResult.Message ?? "خرید پکیج ناموفق بود"),
TransactionId = transaction?.Id ?? 0,
ReferenceCode = verifyResult.RefId ?? string.Empty,
PurchaseInfo = verifyResult.IsSuccess ? new PackagePurchaseInfo
{
PackageId = 1,
PackageName = "پکیج طلایی",
AmountPaid = 5600000,
PurchaseDate = Timestamp.FromDateTime(DateTime.UtcNow),
ExpiryDate = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(365))
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
@@ -1,9 +1,17 @@
using CMSMicroservice.Protobuf.Protos.Products;
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.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;
@@ -13,29 +21,142 @@ namespace CMSMicroservice.WebApi.Services;
public class ProductsService : ProductsContract.ProductsContractBase
{
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
public ProductsService(ISender sender)
public ProductsService(ISender sender, IApplicationDbContext context)
{
_sender = sender;
_context = context;
}
public override async Task<CreateNewProductsResponse> CreateNewProducts(CreateNewProductsRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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<Google.Protobuf.WellKnownTypes.Empty> UpdateProducts(UpdateProductsRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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<Google.Protobuf.WellKnownTypes.Empty> DeleteProducts(DeleteProductsRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
@@ -102,22 +223,256 @@ public class ProductsService : ProductsContract.ProductsContractBase
public override async Task<BulkUpdateProductPricesResponse> BulkUpdateProductPrices(BulkUpdateProductPricesRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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 =============
@@ -279,4 +634,56 @@ public class ProductsService : ProductsContract.ProductsContractBase
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;
}
}
@@ -9,20 +9,35 @@ 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 AppModels = CMSMicroservice.Application.Common.Models;
using Grpc.Core;
using MediatR;
using Mapster;
using Microsoft.EntityFrameworkCore;
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;
public TransactionsService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
public TransactionsService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IPaymentGatewayService paymentGateway)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_paymentGateway = paymentGateway;
}
public override async Task<CreateNewTransactionsResponse> CreateNewTransactions(CreateNewTransactionsRequest request, ServerCallContext context)
{
@@ -121,26 +136,114 @@ public class TransactionsService : TransactionsContract.TransactionsContractBase
public override async Task<CustomerPaymentRequestResponse> CustomerPaymentRequest(CustomerPaymentRequestRequest request, ServerCallContext context)
{
// Mock payment gateway response
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;
await _context.SaveChangesAsync(context.CancellationToken);
return new CustomerPaymentRequestResponse
{
PaymentGWUrl = $"https://payment.gateway.com/payment?amount={request.Amount}&callback={request.CallbackUrl}&description={request.Description}"
PaymentGWUrl = paymentResult.GatewayUrl ?? string.Empty
};
}
public override async Task<CustomerPaymentVerificationResponse> CustomerPaymentVerification(CustomerPaymentVerificationRequest request, ServerCallContext context)
{
// Mock payment verification response
bool isSuccessful = request.Status == "OK";
// 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);
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 = 12345,
PaymentStatus = isSuccessful,
Message = isSuccessful ? "پرداخت با موفقیت انجام شد" : "پرداخت ناموفق",
RefId = isSuccessful ? "REF123456789" : null,
OrderId = "ORDER001",
VerificationStatusCode = isSuccessful ? 101 : 102
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, "لطفاً وارد حساب کاربری خود شوید"));
}
}
@@ -2,10 +2,13 @@ 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 Google.Protobuf.WellKnownTypes;
using Microsoft.EntityFrameworkCore;
using MediatR;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
@@ -13,11 +16,13 @@ public class UserCartsService : UserCartsContract.UserCartsContractBase
{
private readonly IDispatchRequestToCQRS _dispatcher;
private readonly ISender _sender;
private readonly IApplicationDbContext _context;
public UserCartsService(IDispatchRequestToCQRS dispatcher, ISender sender)
public UserCartsService(IDispatchRequestToCQRS dispatcher, ISender sender, IApplicationDbContext context)
{
_dispatcher = dispatcher;
_sender = sender;
_context = context;
}
#region Customer Methods
@@ -117,31 +122,116 @@ public class UserCartsService : UserCartsContract.UserCartsContractBase
public override async Task<AddNewUserCartResponse> AddNewUserCart(
AddNewUserCartRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "AddNewUserCart not implemented yet"));
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<Empty> UpdateUserCart(
UpdateUserCartRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "UpdateUserCart not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "DeleteUserCart not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "GetUserCart not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "GetAllUserCartsByFilter not implemented yet"));
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
@@ -1,7 +1,10 @@
using CMSMicroservice.Application.Common.Authorization;
using CMSMicroservice.Protobuf.Protos.UserOrder;
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;
@@ -15,6 +18,7 @@ using CMSMicroservice.Protobuf.Protos;
using MediatR;
using Mapster;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace CMSMicroservice.WebApi.Services;
@@ -30,21 +34,68 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
_context = context;
_currentUserService = currentUserService;
}
[RequiresPermission(PermissionNames.OrdersCreate)]
public override async Task<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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 };
}
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> UpdateUserOrder(UpdateUserOrderRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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();
}
[RequiresPermission(PermissionNames.OrdersDelete)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> DeleteUserOrder(DeleteUserOrderRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
var query = new GetCustomerOrderQuery
@@ -108,6 +159,7 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
return response;
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<GetAllUserOrderByFilterResponse> GetAllUserOrderByFilter(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
// Admin API - can view all orders or filter by specific user
@@ -342,29 +394,176 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
};
}
[RequiresPermission(PermissionNames.OrdersCancel)]
public override async Task<CancelOrderResponse> CancelOrder(CancelOrderRequest request, ServerCallContext context)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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)
{
throw new RpcException(new Status(StatusCode.Unimplemented, "Method not implemented yet"));
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 =============
@@ -518,13 +717,53 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
public override async Task<CustomerCancelOrderResponse> CustomerCancelOrder(CustomerCancelOrderRequest request, ServerCallContext context)
{
// Mock Customer order cancellation with realistic Persian response
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 = 180000,
RefundTransactionId = "REF" + DateTimeOffset.UtcNow.ToUnixTimeSeconds()
RefundAmount = refundAmount,
RefundTransactionId = $"REF{order.Id}"
};
}
@@ -574,105 +813,105 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
public override async Task<CustomerTrackOrderResponse> CustomerTrackOrder(CustomerTrackOrderRequest request, ServerCallContext context)
{
// Mock Customer order tracking with detailed Persian information
var statusHistory = new List<OrderStatusHistory>
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
{
new OrderStatusHistory
{
Status = OrderStatusEnum.OrderStatusPending,
StatusMessage = "در انتظار تایید",
ChangedAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-5)),
ChangedBy = "سیستم"
},
new OrderStatusHistory
{
Status = OrderStatusEnum.OrderStatusConfirmed,
StatusMessage = "تایید شده",
ChangedAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-4)),
ChangedBy = "کارشناس فروش"
},
new OrderStatusHistory
{
Status = OrderStatusEnum.OrderStatusProcessing,
StatusMessage = "در حال آماده‌سازی",
ChangedAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-3)),
ChangedBy = "انبار"
},
new OrderStatusHistory
{
Status = OrderStatusEnum.OrderStatusShipped,
StatusMessage = "ارسال شده",
ChangedAt = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-2)),
ChangedBy = "پست پیشتاز"
}
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 deliverySteps = new List<DeliveryStep>
var response = new CustomerTrackOrderResponse
{
new DeliveryStep
{
StepName = "دریافت از فروشنده",
StepDescription = "بسته از فروشنده دریافت شد",
StepTime = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-2)),
IsCompleted = true
},
new DeliveryStep
{
StepName = "مرکز پردازش تهران",
StepDescription = "بسته در مرکز پردازش تهران",
StepTime = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-1)),
IsCompleted = true
},
new DeliveryStep
{
StepName = "در حال ارسال",
StepDescription = "بسته در حال ارسال به آدرس مقصد",
StepTime = Timestamp.FromDateTime(DateTime.UtcNow.AddHours(-8)),
IsCompleted = false
}
};
return new CustomerTrackOrderResponse
{
Order = new Protobuf.Protos.UserOrder.CustomerOrderModel
{
Id = request.OrderId,
Amount = 180000,
PackageId = 1,
PackageName = "پکیج ویژه",
Status = OrderStatusEnum.OrderStatusShipped,
StatusMessage = "در حال ارسال",
OrderDate = Timestamp.FromDateTime(DateTime.UtcNow.AddDays(-5)),
TrackingCode = "TRK" + request.OrderId.ToString("000"),
ItemsCount = 4,
CanCancel = false,
CanReorder = true
},
StatusHistory = { statusHistory },
Order = orderModel,
DeliveryInfo = new DeliveryTrackingInfo
{
TrackingCode = "TRK" + request.OrderId.ToString("000"),
TrackingCode = order.TrackingCode ?? string.Empty,
CourierName = "پست پیشتاز",
EstimatedDelivery = "فردا تا ساعت 18:00",
CurrentLocation = "مرکز پخش منطقه 5 تهران",
DeliverySteps = { deliverySteps }
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)
{
// Mock Customer reorder functionality
var newOrderId = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var totalAmount = request.UseCurrentPrices ? 280000 : 250000;
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 = request.UseCurrentPrices ?
"سفارش مجدد با قیمت‌های جدید ثبت شد" :
"سفارش مجدد با قیمت‌های قبلی ثبت شد",
NewOrderId = newOrderId,
Message = "محصولات به سبد خرید اضافه شدند",
NewOrderId = 0, // Cart items added, no order created yet
TotalAmount = totalAmount
};
}
@@ -687,4 +926,17 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
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()
};
}
@@ -16,7 +16,10 @@ 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;
@@ -28,11 +31,25 @@ 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 IFileManagementService _fileManagementService;
public UserService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender sender)
public UserService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender,
IApplicationDbContext context,
ICurrentUserService currentUserService,
IHashService hashService,
IFileManagementService fileManagementService)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
_context = context;
_currentUserService = currentUserService;
_hashService = hashService;
_fileManagementService = fileManagementService;
}
public override async Task<CreateNewUserResponse> CreateNewUser(CreateNewUserRequest request, ServerCallContext context)
{
@@ -90,33 +107,58 @@ public class UserService : UserContract.UserContractBase
public override async Task<GetUserForCustomerResponse> GetUserForCustomer(GetUserForCustomerRequest request, ServerCallContext context)
{
// Mock implementation for Customer Get User
await Task.Delay(10); // Simulate async operation
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 = 123,
FirstName = "احمد",
LastName = "محمدی",
Mobile = "09123456789",
Email = "ahmad.mohammadi@example.com",
NationalCode = "1234567890",
AvatarPath = "/avatars/user_123.jpg",
ParentId = 100,
ReferralCode = "REF123456",
IsMobileVerified = true,
MobileVerifiedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(new DateTime(2024, 1, 15), DateTimeKind.Utc)),
EmailNotifications = true,
SmsNotifications = true,
PushNotifications = false,
BirthDate = Timestamp.FromDateTime(DateTime.SpecifyKind(new DateTime(1990, 5, 20), DateTimeKind.Utc))
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)
{
// Mock implementation for Update Customer Profile
await Task.Delay(10);
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();
}
@@ -153,9 +195,6 @@ public class UserService : UserContract.UserContractBase
public override async Task<ChangeCustomerPasswordResponse> ChangeCustomerPassword(ChangeCustomerPasswordRequest request, ServerCallContext context)
{
// Mock implementation for Change Customer Password
await Task.Delay(10);
if (request.NewPassword != request.ConfirmPassword)
{
return new ChangeCustomerPasswordResponse
@@ -174,6 +213,32 @@ public class UserService : UserContract.UserContractBase
};
}
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,
@@ -233,9 +298,6 @@ public class UserService : UserContract.UserContractBase
public override async Task<UploadCustomerAvatarResponse> UploadCustomerAvatar(UploadCustomerAvatarRequest request, ServerCallContext context)
{
// Mock implementation for Upload Customer Avatar
await Task.Delay(10);
if (request.FileData == null || request.FileData.Length == 0)
{
return new UploadCustomerAvatarResponse
@@ -264,10 +326,36 @@ public class UserService : UserContract.UserContractBase
};
}
// Simulate file upload and generate URL
var fileName = $"avatar_{DateTime.Now.Ticks}.{request.FileMimeType?.Split('/').LastOrDefault()}";
var avatarUrl = $"/uploads/avatars/{fileName}";
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 FMS
var fileBytes = request.FileData.ToByteArray();
var fileName = $"avatar_{userId}_{DateTime.UtcNow.Ticks}";
var mime = request.FileMimeType ?? "image/jpeg";
var avatarUrl = await _fileManagementService.UploadFileAsync(
"Avatars", fileBytes, mime, fileName, context.CancellationToken);
if (string.IsNullOrEmpty(avatarUrl))
{
return new UploadCustomerAvatarResponse
{
Success = false,
Message = "خطا در آپلود فایل. لطفاً مجدد تلاش کنید"
};
}
// Update user avatar path in DB
user.AvatarPath = avatarUrl;
await _context.SaveChangesAsync(context.CancellationToken);
return new UploadCustomerAvatarResponse
{
Success = true,
@@ -295,8 +383,32 @@ public class UserService : UserContract.UserContractBase
public override async Task<Empty> UpdateCustomerSettings(UpdateCustomerSettingsRequest request, ServerCallContext context)
{
// Mock implementation for Update Customer Settings
await Task.Delay(10);
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, "لطفاً وارد حساب کاربری خود شوید"));
}
}
@@ -8,16 +8,29 @@ 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, ISender sender)
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)
{
@@ -100,10 +113,38 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
public override async Task<Google.Protobuf.WellKnownTypes.Empty> CustomerWithdrawBalance(CustomerWithdrawBalanceRequest request, ServerCallContext context)
{
// Mock implementation - would handle withdrawal
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