feat: Implement user permission checks and manual payment functionalities

- Added CheckUserPermissionQuery and CheckUserPermissionQueryHandler for permission validation.
- Introduced GetUserRolesQuery and GetUserRolesQueryHandler to retrieve user roles.
- Created IPermissionService interface and its implementation in PermissionService.
- Defined permission and role constants in PermissionDefinitions.
- Developed SetDefaultVatPercentageCommand and its handler for VAT configuration.
- Implemented GetCurrentVatPercentageQuery and handler to fetch current VAT settings.
- Added manual payment commands: CreateManualPayment, ApproveManualPayment, and RejectManualPayment with respective handlers and validators.
- Created GetManualPaymentsQuery and handler for retrieving manual payment records.
- Integrated gRPC services for manual payments with appropriate permission checks.
- Established Protobuf definitions for manual payment operations and metadata.
This commit is contained in:
masoodafar-web
2025-12-05 17:27:38 +03:30
parent 67b43fea7a
commit 4aa9f28f6e
51 changed files with 1294 additions and 107 deletions

View File

@@ -0,0 +1,6 @@
using MediatR;
namespace BackOffice.BFF.Application.AuthorizationCQ.Queries.CheckUserPermission;
public record CheckUserPermissionQuery(string Permission) : IRequest<bool>;

View File

@@ -0,0 +1,27 @@
using System.Threading;
using System.Threading.Tasks;
using BackOffice.BFF.Application.Common.Interfaces;
using MediatR;
namespace BackOffice.BFF.Application.AuthorizationCQ.Queries.CheckUserPermission;
public class CheckUserPermissionQueryHandler : IRequestHandler<CheckUserPermissionQuery, bool>
{
private readonly IPermissionService _permissionService;
public CheckUserPermissionQueryHandler(IPermissionService permissionService)
{
_permissionService = permissionService;
}
public async Task<bool> Handle(CheckUserPermissionQuery request, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(request.Permission))
{
return true;
}
return await _permissionService.HasPermissionAsync(request.Permission, cancellationToken);
}
}

View File

@@ -0,0 +1,7 @@
using System.Collections.Generic;
using MediatR;
namespace BackOffice.BFF.Application.AuthorizationCQ.Queries.GetUserRoles;
public record GetUserRolesQuery : IRequest<IReadOnlyList<string>>;

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BackOffice.BFF.Application.Common.Interfaces;
using MediatR;
namespace BackOffice.BFF.Application.AuthorizationCQ.Queries.GetUserRoles;
public class GetUserRolesQueryHandler : IRequestHandler<GetUserRolesQuery, IReadOnlyList<string>>
{
private readonly IPermissionService _permissionService;
public GetUserRolesQueryHandler(IPermissionService permissionService)
{
_permissionService = permissionService;
}
public async Task<IReadOnlyList<string>> Handle(GetUserRolesQuery request, CancellationToken cancellationToken)
{
return await _permissionService.GetUserRolesAsync(cancellationToken);
}
}

View File

@@ -61,5 +61,8 @@ public interface IApplicationContractContext
// Public Messages
PublicMessageContract.PublicMessageContractClient PublicMessages { get; }
// Manual Payments (Admin) - CMS gRPC
CMSMicroservice.Protobuf.Protos.ManualPayment.ManualPaymentContract.ManualPaymentContractClient ManualPayments { get; }
#endregion
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace BackOffice.BFF.Application.Common.Interfaces;
public interface IPermissionService
{
Task<IReadOnlyList<string>> GetUserRolesAsync(CancellationToken cancellationToken);
Task<bool> HasPermissionAsync(string permission, CancellationToken cancellationToken);
}

View File

@@ -7,6 +7,8 @@ using CmsUserOrderFilter = CMSMicroservice.Protobuf.Protos.UserOrder.GetAllUserO
using CmsUserOrderRequest = CMSMicroservice.Protobuf.Protos.UserOrder.GetAllUserOrderByFilterRequest;
using GetAllUserOrderByFilterFilter = BackOffice.BFF.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter.GetAllUserOrderByFilterFilter;
using GetAllUserOrderByFilterResponseModel = BackOffice.BFF.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter.GetAllUserOrderByFilterResponseModel;
using CmsGetUserOrderResponse = CMSMicroservice.Protobuf.Protos.UserOrder.GetUserOrderResponse;
using BffGetUserOrderResponseDto = BackOffice.BFF.Application.UserOrderCQ.Queries.GetUserOrder.GetUserOrderResponseDto;
namespace BackOffice.BFF.Application.Common.Mappings;
@@ -26,11 +28,13 @@ public class UserOrderProfile : IRegister
.IgnoreIf((src, dest) => src.Filter.PaymentStatus==null,dest => dest.Filter.PaymentStatus)
.Map(dest => dest.Filter, src => src.Filter == null || IsEmptyFilter(src.Filter) ? null : BuildFilter(src.Filter));
// config.NewConfig< CMSMicroservice.Protobuf.Protos.UserOrder.GetAllUserOrderByFilterResponseModel, GetAllUserOrderByFilterResponseModel>()
//
// .Map(dest => dest.p, src => src.PaymentMethod);
// Map VAT information from CMS GetUserOrderResponse (VatInfo) to flattened DTO fields
config.NewConfig<CmsGetUserOrderResponse, BffGetUserOrderResponseDto>()
.Map(dest => dest.VatBaseAmount, src => src.VatInfo != null ? src.VatInfo.BaseAmount : 0)
.Map(dest => dest.VatAmount, src => src.VatInfo != null ? src.VatInfo.VatAmount : 0)
.Map(dest => dest.VatTotalAmount,
src => src.VatInfo != null ? src.VatInfo.TotalAmount : src.Amount)
.Map(dest => dest.VatPercentage, src => src.VatInfo != null ? src.VatInfo.VatRate * 100 : 0);
}
private static bool IsEmptyFilter(GetAllUserOrderByFilterFilter src)

View File

@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace BackOffice.BFF.Application.Common.Models;
public static class RoleNames
{
public const string SuperAdmin = "SuperAdmin";
public const string Admin = "Admin";
public const string Inspector = "Inspector";
}
public static class PermissionNames
{
// Dashboard
public const string DashboardView = "dashboard.view";
// Orders
public const string OrdersView = "orders.view";
public const string OrdersCreate = "orders.create";
public const string OrdersUpdate = "orders.update";
public const string OrdersDelete = "orders.delete";
public const string OrdersCancel = "orders.cancel";
public const string OrdersApprove = "orders.approve";
// Products
public const string ProductsView = "products.view";
public const string ProductsCreate = "products.create";
public const string ProductsUpdate = "products.update";
public const string ProductsDelete = "products.delete";
// Users
public const string UsersView = "users.view";
public const string UsersUpdate = "users.update";
public const string UsersDelete = "users.delete";
// Commission / Withdrawal
public const string CommissionView = "commission.view";
public const string CommissionApproveWithdrawal = "commission.approve_withdrawal";
// Public Messages
public const string PublicMessagesView = "publicmessages.view";
public const string PublicMessagesCreate = "publicmessages.create";
public const string PublicMessagesUpdate = "publicmessages.update";
public const string PublicMessagesPublish = "publicmessages.publish";
// Manual Payments
public const string ManualPaymentsView = "manualpayments.view";
public const string ManualPaymentsCreate = "manualpayments.create";
public const string ManualPaymentsApprove = "manualpayments.approve";
// Settings / Configuration / VAT
public const string SettingsView = "settings.view";
public const string SettingsManageConfiguration = "settings.manage_configuration";
public const string SettingsManageVat = "settings.manage_vat";
// Reports
public const string ReportsView = "reports.view";
}
public static class RolePermissionConfig
{
private static readonly IReadOnlyDictionary<string, string[]> RolePermissions =
new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
{
// SuperAdmin: full access (wildcard)
[RoleNames.SuperAdmin] = new[] { "*" },
// Admin: مدیریت سفارش‌ها، محصولات، بخشی از کمیسیون و پیام‌ها
[RoleNames.Admin] = new[]
{
PermissionNames.DashboardView,
PermissionNames.OrdersView,
PermissionNames.OrdersCreate,
PermissionNames.OrdersUpdate,
PermissionNames.OrdersCancel,
PermissionNames.ProductsView,
PermissionNames.ProductsCreate,
PermissionNames.ProductsUpdate,
PermissionNames.ProductsDelete,
PermissionNames.UsersView,
PermissionNames.UsersUpdate,
PermissionNames.CommissionView,
PermissionNames.CommissionApproveWithdrawal,
PermissionNames.PublicMessagesView,
PermissionNames.PublicMessagesCreate,
PermissionNames.PublicMessagesUpdate,
PermissionNames.PublicMessagesPublish,
PermissionNames.ManualPaymentsView,
PermissionNames.ManualPaymentsCreate,
PermissionNames.ReportsView
},
// Inspector: فقط مشاهده
[RoleNames.Inspector] = new[]
{
PermissionNames.DashboardView,
PermissionNames.OrdersView,
PermissionNames.UsersView,
PermissionNames.CommissionView,
PermissionNames.PublicMessagesView,
PermissionNames.ReportsView
}
};
public static bool HasPermission(string role, string permission)
{
if (string.IsNullOrWhiteSpace(role) || string.IsNullOrWhiteSpace(permission))
{
return false;
}
if (!RolePermissions.TryGetValue(role, out var permissions))
{
return false;
}
if (permissions.Contains("*", StringComparer.OrdinalIgnoreCase))
{
return true;
}
return permissions.Contains(permission, StringComparer.OrdinalIgnoreCase);
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.SetDefaultVatPercentage;
public record SetDefaultVatPercentageCommand : IRequest<Unit>
{
/// <summary>
/// درصد VAT جدید (مثلاً 10 به معنای 10٪)
/// </summary>
public int VatPercentage { get; init; }
}

View File

@@ -0,0 +1,43 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.Configuration.Protobuf;
using Google.Protobuf.WellKnownTypes;
using MediatR;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.SetDefaultVatPercentage;
public class SetDefaultVatPercentageCommandHandler : IRequestHandler<SetDefaultVatPercentageCommand, Unit>
{
private const string VatConfigurationKey = "DefaultVatPercentage";
private readonly IApplicationContractContext _context;
private readonly ILogger<SetDefaultVatPercentageCommandHandler> _logger;
public SetDefaultVatPercentageCommandHandler(
IApplicationContractContext context,
ILogger<SetDefaultVatPercentageCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<Unit> Handle(SetDefaultVatPercentageCommand request, CancellationToken cancellationToken)
{
var grpcRequest = new CreateOrUpdateConfigurationRequest
{
Key = VatConfigurationKey,
Value = request.VatPercentage.ToString(),
Scope = 0, // System scope
Description = new StringValue
{
Value = "درصد پیش‌فرض مالیات بر ارزش افزوده سفارش‌ها"
}
};
await _context.Configurations.CreateOrUpdateConfigurationAsync(grpcRequest, cancellationToken: cancellationToken);
_logger.LogInformation("Default VAT percentage updated to {VatPercentage}%.", request.VatPercentage);
return Unit.Value;
}
}

View File

@@ -0,0 +1,14 @@
using FluentValidation;
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.SetDefaultVatPercentage;
public class SetDefaultVatPercentageCommandValidator : AbstractValidator<SetDefaultVatPercentageCommand>
{
public SetDefaultVatPercentageCommandValidator()
{
RuleFor(x => x.VatPercentage)
.InclusiveBetween(0, 100)
.WithMessage("درصد VAT باید بین 0 تا 100 باشد.");
}
}

View File

@@ -0,0 +1,17 @@
namespace BackOffice.BFF.Application.ConfigurationCQ.Queries.GetCurrentVatPercentage;
public record GetCurrentVatPercentageQuery : IRequest<GetCurrentVatPercentageResponse>;
public class GetCurrentVatPercentageResponse
{
/// <summary>
/// درصد VAT فعلی (مثلاً 10 به معنای 10٪)
/// </summary>
public int VatPercentage { get; set; }
/// <summary>
/// آیا مقدار از تنظیمات خوانده شده (true) یا مقدار پیش‌فرض استفاده شده (false)
/// </summary>
public bool IsConfigured { get; set; }
}

View File

@@ -0,0 +1,70 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.Configuration.Protobuf;
using MediatR;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.ConfigurationCQ.Queries.GetCurrentVatPercentage;
public class GetCurrentVatPercentageQueryHandler : IRequestHandler<GetCurrentVatPercentageQuery, GetCurrentVatPercentageResponse>
{
private const string VatConfigurationKey = "DefaultVatPercentage";
private readonly IApplicationContractContext _context;
private readonly ILogger<GetCurrentVatPercentageQueryHandler> _logger;
public GetCurrentVatPercentageQueryHandler(
IApplicationContractContext context,
ILogger<GetCurrentVatPercentageQueryHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<GetCurrentVatPercentageResponse> Handle(GetCurrentVatPercentageQuery request, CancellationToken cancellationToken)
{
try
{
var grpcRequest = new GetConfigurationByKeyRequest
{
Key = VatConfigurationKey
};
var response = await _context.Configurations.GetConfigurationByKeyAsync(grpcRequest, cancellationToken: cancellationToken);
if (response == null || string.IsNullOrWhiteSpace(response.Value))
{
_logger.LogInformation("VAT configuration not found. Using default value 10%.");
return new GetCurrentVatPercentageResponse
{
VatPercentage = 10,
IsConfigured = false
};
}
if (!int.TryParse(response.Value, out var vat) || vat < 0)
{
_logger.LogWarning("Invalid VAT configuration value '{Value}'. Falling back to default 10%.", response.Value);
return new GetCurrentVatPercentageResponse
{
VatPercentage = 10,
IsConfigured = false
};
}
return new GetCurrentVatPercentageResponse
{
VatPercentage = vat,
IsConfigured = true
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while reading VAT configuration. Falling back to default value 10%.");
return new GetCurrentVatPercentageResponse
{
VatPercentage = 10,
IsConfigured = false
};
}
}
}

View File

@@ -0,0 +1,10 @@
using MediatR;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.ApproveManualPayment;
public class ApproveManualPaymentCommand : IRequest<bool>
{
public long ManualPaymentId { get; set; }
public string? ApprovalNote { get; set; }
}

View File

@@ -0,0 +1,41 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using Google.Protobuf.WellKnownTypes;
using MediatR;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.ApproveManualPayment;
public class ApproveManualPaymentCommandHandler : IRequestHandler<ApproveManualPaymentCommand, bool>
{
private readonly IApplicationContractContext _context;
private readonly ILogger<ApproveManualPaymentCommandHandler> _logger;
public ApproveManualPaymentCommandHandler(
IApplicationContractContext context,
ILogger<ApproveManualPaymentCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<bool> Handle(ApproveManualPaymentCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Approving manual payment via BFF. Id: {Id}", request.ManualPaymentId);
var grpcRequest = new ApproveManualPaymentRequest
{
ManualPaymentId = request.ManualPaymentId
};
if (!string.IsNullOrWhiteSpace(request.ApprovalNote))
{
grpcRequest.ApprovalNote = new StringValue { Value = request.ApprovalNote };
}
await _context.ManualPayments.ApproveManualPaymentAsync(grpcRequest, cancellationToken: cancellationToken);
return true;
}
}

View File

@@ -0,0 +1,14 @@
using FluentValidation;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.ApproveManualPayment;
public class ApproveManualPaymentCommandValidator : AbstractValidator<ApproveManualPaymentCommand>
{
public ApproveManualPaymentCommandValidator()
{
RuleFor(x => x.ManualPaymentId)
.GreaterThan(0)
.WithMessage("شناسه پرداخت دستی نامعتبر است.");
}
}

View File

@@ -0,0 +1,18 @@
using MediatR;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.CreateManualPayment;
public class CreateManualPaymentCommand : IRequest<CreateManualPaymentResponseDto>
{
public long UserId { get; set; }
public long Amount { get; set; }
public int Type { get; set; }
public string Description { get; set; } = string.Empty;
public string? ReferenceNumber { get; set; }
}
public class CreateManualPaymentResponseDto
{
public long Id { get; set; }
}

View File

@@ -0,0 +1,51 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using Google.Protobuf.WellKnownTypes;
using MediatR;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.CreateManualPayment;
public class CreateManualPaymentCommandHandler : IRequestHandler<CreateManualPaymentCommand, CreateManualPaymentResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ILogger<CreateManualPaymentCommandHandler> _logger;
public CreateManualPaymentCommandHandler(
IApplicationContractContext context,
ILogger<CreateManualPaymentCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<CreateManualPaymentResponseDto> Handle(CreateManualPaymentCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation(
"Creating manual payment via BFF for UserId {UserId}, Amount {Amount}, Type {Type}",
request.UserId,
request.Amount,
request.Type);
var grpcRequest = new CreateManualPaymentRequest
{
UserId = request.UserId,
Amount = request.Amount,
Type = (ManualPaymentType)request.Type,
Description = request.Description
};
if (!string.IsNullOrWhiteSpace(request.ReferenceNumber))
{
grpcRequest.ReferenceNumber = new StringValue { Value = request.ReferenceNumber };
}
var response = await _context.ManualPayments.CreateManualPaymentAsync(grpcRequest, cancellationToken: cancellationToken);
return new CreateManualPaymentResponseDto
{
Id = response.Id
};
}
}

View File

@@ -0,0 +1,26 @@
using FluentValidation;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.CreateManualPayment;
public class CreateManualPaymentCommandValidator : AbstractValidator<CreateManualPaymentCommand>
{
public CreateManualPaymentCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0)
.WithMessage("شناسه کاربر باید بزرگتر از صفر باشد.");
RuleFor(x => x.Amount)
.GreaterThan(0)
.WithMessage("مبلغ باید بزرگتر از صفر باشد.");
RuleFor(x => x.Description)
.NotEmpty()
.WithMessage("توضیحات الزامی است.");
RuleFor(x => x.Type)
.GreaterThan(0)
.WithMessage("نوع پرداخت دستی نامعتبر است.");
}
}

View File

@@ -0,0 +1,10 @@
using MediatR;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.RejectManualPayment;
public class RejectManualPaymentCommand : IRequest<bool>
{
public long ManualPaymentId { get; set; }
public string RejectionReason { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,36 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using MediatR;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.RejectManualPayment;
public class RejectManualPaymentCommandHandler : IRequestHandler<RejectManualPaymentCommand, bool>
{
private readonly IApplicationContractContext _context;
private readonly ILogger<RejectManualPaymentCommandHandler> _logger;
public RejectManualPaymentCommandHandler(
IApplicationContractContext context,
ILogger<RejectManualPaymentCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<bool> Handle(RejectManualPaymentCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("Rejecting manual payment via BFF. Id: {Id}", request.ManualPaymentId);
var grpcRequest = new RejectManualPaymentRequest
{
ManualPaymentId = request.ManualPaymentId,
RejectionReason = request.RejectionReason
};
await _context.ManualPayments.RejectManualPaymentAsync(grpcRequest, cancellationToken: cancellationToken);
return true;
}
}

View File

@@ -0,0 +1,18 @@
using FluentValidation;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Commands.RejectManualPayment;
public class RejectManualPaymentCommandValidator : AbstractValidator<RejectManualPaymentCommand>
{
public RejectManualPaymentCommandValidator()
{
RuleFor(x => x.ManualPaymentId)
.GreaterThan(0)
.WithMessage("شناسه پرداخت دستی نامعتبر است.");
RuleFor(x => x.RejectionReason)
.NotEmpty()
.WithMessage("دلیل رد الزامی است.");
}
}

View File

@@ -0,0 +1,15 @@
using MediatR;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Queries.GetManualPayments;
public class GetManualPaymentsQuery : IRequest<GetManualPaymentsResponseDto>
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 10;
public long? UserId { get; set; }
public int? Status { get; set; }
public int? Type { get; set; }
public long? RequestedBy { get; set; }
public bool OrderByDescending { get; set; } = true;
public string? ReferenceNumber { get; set; }
}

View File

@@ -0,0 +1,114 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.Application.Common.Models;
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using Google.Protobuf.WellKnownTypes;
using MediatR;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Queries.GetManualPayments;
public class GetManualPaymentsQueryHandler : IRequestHandler<GetManualPaymentsQuery, GetManualPaymentsResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ILogger<GetManualPaymentsQueryHandler> _logger;
public GetManualPaymentsQueryHandler(
IApplicationContractContext context,
ILogger<GetManualPaymentsQueryHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<GetManualPaymentsResponseDto> Handle(GetManualPaymentsQuery request, CancellationToken cancellationToken)
{
_logger.LogInformation(
"Getting manual payments via BFF. Page: {Page}, PageSize: {PageSize}, Status: {Status}, Type: {Type}",
request.PageNumber,
request.PageSize,
request.Status,
request.Type);
var grpcRequest = new GetAllManualPaymentsRequest
{
PageNumber = request.PageNumber,
PageSize = request.PageSize
};
if (request.UserId.HasValue)
{
grpcRequest.UserId = new Int64Value { Value = request.UserId.Value };
}
if (request.Status.HasValue)
{
grpcRequest.Status = new Int32Value { Value = request.Status.Value };
}
if (request.Type.HasValue)
{
grpcRequest.Type = new Int32Value { Value = request.Type.Value };
}
if (request.RequestedBy.HasValue)
{
grpcRequest.RequestedBy = new Int64Value { Value = request.RequestedBy.Value };
}
grpcRequest.OrderByDescending = new BoolValue { Value = request.OrderByDescending };
var response = await _context.ManualPayments.GetAllManualPaymentsAsync(grpcRequest, cancellationToken: cancellationToken);
var meta = response.MetaData;
var models = response.Models?
.Select(m => new ManualPaymentModel
{
Id = m.Id,
UserId = m.UserId,
UserFullName = m.UserFullName,
UserMobile = m.UserMobile,
Amount = m.Amount,
Type = (int)m.Type,
TypeDisplay = m.TypeDisplay,
Description = m.Description,
ReferenceNumber = m.ReferenceNumber,
Status = (int)m.Status,
StatusDisplay = m.StatusDisplay,
RequestedBy = m.RequestedBy,
RequestedByName = m.RequestedByName,
ApprovedBy = m.ApprovedBy?.Value,
ApprovedByName = m.ApprovedByName,
ApprovedAt = m.ApprovedAt?.ToDateTime(),
RejectionReason = m.RejectionReason,
TransactionId = m.TransactionId?.Value,
Created = m.Created.ToDateTime()
})
.ToList()
?? new List<ManualPaymentModel>();
if (!string.IsNullOrWhiteSpace(request.ReferenceNumber))
{
models = models
.Where(m => !string.IsNullOrWhiteSpace(m.ReferenceNumber) &&
m.ReferenceNumber.Contains(request.ReferenceNumber!, StringComparison.OrdinalIgnoreCase))
.ToList();
}
var result = new GetManualPaymentsResponseDto
{
MetaData = new MetaData
{
CurrentPage = meta.CurrentPage,
TotalPage = meta.TotalPage,
PageSize = meta.PageSize,
TotalCount = meta.TotalCount,
HasPrevious = meta.HasPrevious,
HasNext = meta.HasNext
},
Models = models
};
return result;
}
}

View File

@@ -0,0 +1,33 @@
using BackOffice.BFF.Application.Common.Models;
namespace BackOffice.BFF.Application.ManualPaymentCQ.Queries.GetManualPayments;
public class GetManualPaymentsResponseDto
{
public MetaData MetaData { get; set; } = new();
public List<ManualPaymentModel>? Models { get; set; }
}
public class ManualPaymentModel
{
public long Id { get; set; }
public long UserId { get; set; }
public string UserFullName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public long Amount { get; set; }
public int Type { get; set; }
public string TypeDisplay { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public string? ReferenceNumber { get; set; }
public int Status { get; set; }
public string StatusDisplay { get; set; } = string.Empty;
public long RequestedBy { get; set; }
public string RequestedByName { get; set; } = string.Empty;
public long? ApprovedBy { get; set; }
public string? ApprovedByName { get; set; }
public DateTime? ApprovedAt { get; set; }
public string? RejectionReason { get; set; }
public long? TransactionId { get; set; }
public DateTime Created { get; set; }
}

View File

@@ -1,12 +1,12 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.Package.Protobuf.Protos.Package;
using CMSMicroservice.Protobuf.Protos.Package;
using MediatR;
using Microsoft.Extensions.Logging;
using BffPackage = BackOffice.BFF.Package.Protobuf.Protos.Package;
using CmsPackage = CMSMicroservice.Protobuf.Protos.Package;
namespace BackOffice.BFF.Application.PackageCQ.Queries.GetUserPackageStatus;
public class GetUserPackageStatusQueryHandler : IRequestHandler<GetUserPackageStatusQuery, GetUserPackageStatusResponse>
public class GetUserPackageStatusQueryHandler : IRequestHandler<GetUserPackageStatusQuery, BffPackage.GetUserPackageStatusResponse>
{
private readonly IApplicationContractContext _context;
private readonly ILogger<GetUserPackageStatusQueryHandler> _logger;
@@ -19,9 +19,9 @@ public class GetUserPackageStatusQueryHandler : IRequestHandler<GetUserPackageSt
_logger = logger;
}
public async Task<GetUserPackageStatusResponse> Handle(GetUserPackageStatusQuery request, CancellationToken cancellationToken)
public async Task<BffPackage.GetUserPackageStatusResponse> Handle(GetUserPackageStatusQuery request, CancellationToken cancellationToken)
{
var grpcRequest = new GetUserPackageStatusRequest
var grpcRequest = new CmsPackage.GetUserPackageStatusRequest
{
UserId = request.UserId
};
@@ -30,7 +30,7 @@ public class GetUserPackageStatusQueryHandler : IRequestHandler<GetUserPackageSt
_logger.LogInformation("Retrieved package status for user {UserId}", request.UserId);
var result = new GetUserPackageStatusResponse
var result = new BffPackage.GetUserPackageStatusResponse
{
UserId = response.UserId,
PackagePurchaseMethod = response.PackagePurchaseMethod,

View File

@@ -1,10 +1,15 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.PublicMessage.Protobuf;
using CMSMicroservice.Protobuf.Protos;
using BffProto = BackOffice.BFF.PublicMessage.Protobuf;
using CmsProto = CMSMicroservice.Protobuf.Protos;
namespace BackOffice.BFF.Application.PublicMessageCQ.Commands.ArchiveMessage;
public class ArchiveMessageCommandHandler : IRequestHandler<ArchiveMessageRequest, ArchiveMessageResponse>
public class ArchiveMessageCommand : IRequest<BffProto.ArchiveMessageResponse>
{
public long MessageId { get; set; }
}
public class ArchiveMessageCommandHandler : IRequestHandler<ArchiveMessageCommand, BffProto.ArchiveMessageResponse>
{
private readonly IApplicationContractContext _context;
@@ -13,9 +18,9 @@ public class ArchiveMessageCommandHandler : IRequestHandler<ArchiveMessageReques
_context = context;
}
public async Task<ArchiveMessageResponse> Handle(ArchiveMessageRequest request, CancellationToken cancellationToken)
public async Task<BffProto.ArchiveMessageResponse> Handle(ArchiveMessageCommand request, CancellationToken cancellationToken)
{
var cmsRequest = new ArchiveMessageRequest
var cmsRequest = new CmsProto.ArchiveMessageRequest
{
MessageId = request.MessageId
};
@@ -24,7 +29,7 @@ public class ArchiveMessageCommandHandler : IRequestHandler<ArchiveMessageReques
cmsRequest,
cancellationToken: cancellationToken);
return new ArchiveMessageResponse
return new BffProto.ArchiveMessageResponse
{
Success = cmsResponse.Success,
Message = cmsResponse.Message,

View File

@@ -1,11 +1,11 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.PublicMessage.Protobuf;
using CMSMicroservice.Protobuf.Protos;
using Google.Protobuf.WellKnownTypes;
using BffProto = BackOffice.BFF.PublicMessage.Protobuf;
using CmsProto = CMSMicroservice.Protobuf.Protos;
namespace BackOffice.BFF.Application.PublicMessageCQ.Commands.CreatePublicMessage;
public class CreatePublicMessageCommandHandler : IRequestHandler<CreatePublicMessageRequest, CreatePublicMessageResponse>
public class CreatePublicMessageCommandHandler : IRequestHandler<BffProto.CreatePublicMessageRequest, BffProto.CreatePublicMessageResponse>
{
private readonly IApplicationContractContext _context;
@@ -14,9 +14,9 @@ public class CreatePublicMessageCommandHandler : IRequestHandler<CreatePublicMes
_context = context;
}
public async Task<CreatePublicMessageResponse> Handle(CreatePublicMessageRequest request, CancellationToken cancellationToken)
public async Task<BffProto.CreatePublicMessageResponse> Handle(BffProto.CreatePublicMessageRequest request, CancellationToken cancellationToken)
{
var cmsRequest = new CreatePublicMessageRequest
var cmsRequest = new CmsProto.CreatePublicMessageRequest
{
Title = request.Title,
Content = request.Content,
@@ -30,7 +30,7 @@ public class CreatePublicMessageCommandHandler : IRequestHandler<CreatePublicMes
cmsRequest,
cancellationToken: cancellationToken);
return new CreatePublicMessageResponse
return new BffProto.CreatePublicMessageResponse
{
Success = true,
Message = "پیام با موفقیت ایجاد شد",

View File

@@ -1,10 +1,11 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.PublicMessage.Protobuf;
using CMSMicroservice.Protobuf.Protos;
using MediatR;
using BffProto = BackOffice.BFF.PublicMessage.Protobuf;
using CmsProto = CMSMicroservice.Protobuf.Protos;
namespace BackOffice.BFF.Application.PublicMessageCQ.Commands.DeletePublicMessage;
public class DeletePublicMessageCommandHandler : IRequestHandler<DeletePublicMessageRequest, DeletePublicMessageRequest>
public class DeletePublicMessageCommandHandler : IRequestHandler<BffProto.DeletePublicMessageRequest, BffProto.DeletePublicMessageRequest>
{
private readonly IApplicationContractContext _context;
@@ -13,9 +14,9 @@ public class DeletePublicMessageCommandHandler : IRequestHandler<DeletePublicMes
_context = context;
}
public async Task<DeletePublicMessageRequest> Handle(DeletePublicMessageRequest request, CancellationToken cancellationToken)
public async Task<BffProto.DeletePublicMessageRequest> Handle(BffProto.DeletePublicMessageRequest request, CancellationToken cancellationToken)
{
var cmsRequest = new DeletePublicMessageRequest
var cmsRequest = new CmsProto.DeletePublicMessageRequest
{
MessageId = request.MessageId
};

View File

@@ -1,10 +1,11 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.PublicMessage.Protobuf;
using CMSMicroservice.Protobuf.Protos;
using MediatR;
using BffProto = BackOffice.BFF.PublicMessage.Protobuf;
using CmsProto = CMSMicroservice.Protobuf.Protos;
namespace BackOffice.BFF.Application.PublicMessageCQ.Commands.PublishMessage;
public class PublishMessageCommandHandler : IRequestHandler<PublishMessageRequest, PublishMessageResponse>
public class PublishMessageCommandHandler : IRequestHandler<BffProto.PublishMessageRequest, BffProto.PublishMessageResponse>
{
private readonly IApplicationContractContext _context;
@@ -13,9 +14,9 @@ public class PublishMessageCommandHandler : IRequestHandler<PublishMessageReques
_context = context;
}
public async Task<PublishMessageResponse> Handle(PublishMessageRequest request, CancellationToken cancellationToken)
public async Task<BffProto.PublishMessageResponse> Handle(BffProto.PublishMessageRequest request, CancellationToken cancellationToken)
{
var cmsRequest = new PublishMessageRequest
var cmsRequest = new CmsProto.PublishMessageRequest
{
MessageId = request.MessageId
};
@@ -24,7 +25,7 @@ public class PublishMessageCommandHandler : IRequestHandler<PublishMessageReques
cmsRequest,
cancellationToken: cancellationToken);
return new PublishMessageResponse
return new BffProto.PublishMessageResponse
{
Success = cmsResponse.Success,
Message = cmsResponse.Message,

View File

@@ -1,11 +1,12 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.PublicMessage.Protobuf;
using CMSMicroservice.Protobuf.Protos;
using Google.Protobuf.WellKnownTypes;
using MediatR;
using BffProto = BackOffice.BFF.PublicMessage.Protobuf;
using CmsProto = CMSMicroservice.Protobuf.Protos;
namespace BackOffice.BFF.Application.PublicMessageCQ.Commands.UpdatePublicMessage;
public class UpdatePublicMessageCommandHandler : IRequestHandler<UpdatePublicMessageRequest, UpdatePublicMessageRequest>
public class UpdatePublicMessageCommandHandler : IRequestHandler<BffProto.UpdatePublicMessageRequest, BffProto.UpdatePublicMessageRequest>
{
private readonly IApplicationContractContext _context;
@@ -14,9 +15,9 @@ public class UpdatePublicMessageCommandHandler : IRequestHandler<UpdatePublicMes
_context = context;
}
public async Task<UpdatePublicMessageRequest> Handle(UpdatePublicMessageRequest request, CancellationToken cancellationToken)
public async Task<BffProto.UpdatePublicMessageRequest> Handle(BffProto.UpdatePublicMessageRequest request, CancellationToken cancellationToken)
{
var cmsRequest = new UpdatePublicMessageRequest
var cmsRequest = new CmsProto.UpdatePublicMessageRequest
{
Id = request.MessageId,
Title = request.Title,

View File

@@ -1,19 +1,18 @@
// GetActiveMessages - Public view
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.PublicMessage.Protobuf;
using CMSMicroservice.Protobuf.Protos;
using Google.Protobuf.WellKnownTypes;
using MediatR;
using BffProto = BackOffice.BFF.PublicMessage.Protobuf;
using CmsProto = CMSMicroservice.Protobuf.Protos;
namespace BackOffice.BFF.Application.PublicMessageCQ.Queries.GetActiveMessages;
public record GetActiveMessagesQuery : IRequest<GetActiveMessagesResponse>
public record GetActiveMessagesQuery : IRequest<BffProto.GetActiveMessagesResponse>
{
public int? MessageType { get; init; }
public string? TargetAudience { get; init; }
}
public class GetActiveMessagesQueryHandler : IRequestHandler<GetActiveMessagesQuery, GetActiveMessagesResponse>
public class GetActiveMessagesQueryHandler : IRequestHandler<GetActiveMessagesQuery, BffProto.GetActiveMessagesResponse>
{
private readonly IApplicationContractContext _context;
@@ -22,18 +21,18 @@ public class GetActiveMessagesQueryHandler : IRequestHandler<GetActiveMessagesQu
_context = context;
}
public async Task<GetActiveMessagesResponse> Handle(GetActiveMessagesQuery request, CancellationToken cancellationToken)
public async Task<BffProto.GetActiveMessagesResponse> Handle(GetActiveMessagesQuery request, CancellationToken cancellationToken)
{
var cmsRequest = new GetActiveMessagesRequest();
var cmsRequest = new CmsProto.GetActiveMessagesRequest();
// نسخه فعلی CMS فیلدی برای فیلتر ندارد؛ در صورت اضافه شدن، اینجا مپ می‌شود.
var cmsResponse = await _context.PublicMessages.GetActiveMessagesAsync(cmsRequest, cancellationToken: cancellationToken);
var result = new GetActiveMessagesResponse();
var result = new BffProto.GetActiveMessagesResponse();
foreach (var message in cmsResponse.Messages)
{
result.Messages.Add(new PublicMessageDto
result.Messages.Add(new BffProto.PublicMessageDto
{
MessageId = message.Id,
Title = message.Title,

View File

@@ -1,13 +1,13 @@
// GetAllMessages - Admin view
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.PublicMessage.Protobuf;
using CMSMicroservice.Protobuf.Protos;
using Google.Protobuf.WellKnownTypes;
using MediatR;
using BffProto = BackOffice.BFF.PublicMessage.Protobuf;
using CmsProto = CMSMicroservice.Protobuf.Protos;
namespace BackOffice.BFF.Application.PublicMessageCQ.Queries.GetAllMessages;
public record GetAllMessagesQuery : IRequest<GetAllMessagesResponse>
public record GetAllMessagesQuery : IRequest<BffProto.GetAllMessagesResponse>
{
public int? Status { get; init; }
public int? MessageType { get; init; }
@@ -15,7 +15,7 @@ public record GetAllMessagesQuery : IRequest<GetAllMessagesResponse>
public int PageSize { get; init; }
}
public class GetAllMessagesQueryHandler : IRequestHandler<GetAllMessagesQuery, GetAllMessagesResponse>
public class GetAllMessagesQueryHandler : IRequestHandler<GetAllMessagesQuery, BffProto.GetAllMessagesResponse>
{
private readonly IApplicationContractContext _context;
@@ -24,9 +24,9 @@ public class GetAllMessagesQueryHandler : IRequestHandler<GetAllMessagesQuery, G
_context = context;
}
public async Task<GetAllMessagesResponse> Handle(GetAllMessagesQuery request, CancellationToken cancellationToken)
public async Task<BffProto.GetAllMessagesResponse> Handle(GetAllMessagesQuery request, CancellationToken cancellationToken)
{
var cmsRequest = new GetAllMessagesRequest
var cmsRequest = new CmsProto.GetAllMessagesRequest
{
PageNumber = request.PageNumber,
PageSize = request.PageSize
@@ -44,7 +44,7 @@ public class GetAllMessagesQueryHandler : IRequestHandler<GetAllMessagesQuery, G
var cmsResponse = await _context.PublicMessages.GetAllMessagesAsync(cmsRequest, cancellationToken: cancellationToken);
var result = new GetAllMessagesResponse
var result = new BffProto.GetAllMessagesResponse
{
TotalCount = (int)cmsResponse.MetaData.TotalCount,
PageNumber = request.PageNumber,
@@ -53,7 +53,7 @@ public class GetAllMessagesQueryHandler : IRequestHandler<GetAllMessagesQuery, G
foreach (var message in cmsResponse.Messages)
{
result.Messages.Add(new AdminPublicMessageDto
result.Messages.Add(new BffProto.AdminPublicMessageDto
{
MessageId = message.Id,
Title = message.Title,

View File

@@ -15,53 +15,3 @@ public class CancelOrderCommandValidator : AbstractValidator<CancelOrderCommand>
.WithMessage("دلیل لغو سفارش الزامی است");
}
}
*** Add File: BackOffice.BFF/src/BackOffice.BFF.Application/UserOrderCQ/Commands/CancelOrder/CancelOrderCommandHandler.cs
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserOrder;
using MediatR;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.UserOrderCQ.Commands.CancelOrder;
public class CancelOrderCommandHandler : IRequestHandler<CancelOrderCommand, CancelOrderResponse>
{
private readonly IApplicationContractContext _context;
private readonly ILogger<CancelOrderCommandHandler> _logger;
public CancelOrderCommandHandler(
IApplicationContractContext context,
ILogger<CancelOrderCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<CancelOrderResponse> Handle(CancelOrderCommand request, CancellationToken cancellationToken)
{
var grpcRequest = new CMSMicroservice.Protobuf.Protos.UserOrder.CancelOrderRequest
{
OrderId = request.OrderId,
CancelReason = request.CancelReason ?? string.Empty,
RefundPayment = request.RefundPayment
};
var response = await _context.UserOrders.CancelOrderAsync(grpcRequest, cancellationToken: cancellationToken);
_logger.LogInformation(
"Cancelled order {OrderId}. Status={Status} RefundProcessed={RefundProcessed}",
response.OrderId,
response.Status,
response.RefundProcessed);
return new CancelOrderResponse
{
OrderId = response.OrderId,
Status = (int)response.Status,
Message = response.Message,
RefundProcessed = response.RefundProcessed
};
}
}

View File

@@ -43,4 +43,8 @@ public class GetAllUserOrderByFilterResponseModel
public string? UserNationalCode { get; set; }
// روش پرداخت (0=IPG,1=Wallet)
public int PaymentMethod { get; set; }
// مبلغ مالیات بر ارزش افزوده (ریال)
public long VatAmount { get; set; }
// درصد مالیات بر ارزش افزوده (مثلاً 9 برای 9٪)
public double VatPercentage { get; set; }
}

View File

@@ -36,6 +36,14 @@ public class GetUserOrderResponseDto
public string? UserFullName { get; set; }
// کدملی کاربر
public string? UserNationalCode { get; set; }
// مبلغ پایه (قبل از مالیات)
public long VatBaseAmount { get; set; }
// مبلغ مالیات بر ارزش افزوده (ریال)
public long VatAmount { get; set; }
// مبلغ نهایی (شامل مالیات)
public long VatTotalAmount { get; set; }
// درصد مالیات بر ارزش افزوده (مثلاً 9 برای 9٪)
public double VatPercentage { get; set; }
}
public class GetUserOrderResponseFactorDetail

View File

@@ -14,6 +14,7 @@ public static class ConfigureServices
{
services.AddSingleton<IAfrinoIdpService, AfrinoIdpService>();
services.AddSingleton<IApplicationContractContext, ApplicationContractContext>();
services.AddScoped<IPermissionService, PermissionService>();
services.AddInfrastructureGrpcServices(configuration);
#region AddAuthentication
@@ -89,4 +90,4 @@ public static class ConfigureServices
return services;
}
}
}

View File

@@ -22,6 +22,7 @@ using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using CMSMicroservice.Protobuf.Protos.Tag;
using CMSMicroservice.Protobuf.Protos.ProductTag;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using Microsoft.Extensions.DependencyInjection;
namespace BackOffice.BFF.Infrastructure.Services;
@@ -84,5 +85,8 @@ public class ApplicationContractContext : IApplicationContractContext
// Public Messages
public PublicMessageContract.PublicMessageContractClient PublicMessages => GetService<PublicMessageContract.PublicMessageContractClient>();
// Manual Payments (Admin)
public ManualPaymentContract.ManualPaymentContractClient ManualPayments => GetService<ManualPaymentContract.ManualPaymentContractClient>();
#endregion
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.Application.Common.Models;
using Microsoft.AspNetCore.Http;
namespace BackOffice.BFF.Infrastructure.Services;
public class PermissionService : IPermissionService
{
private readonly IHttpContextAccessor _httpContextAccessor;
public PermissionService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public Task<IReadOnlyList<string>> GetUserRolesAsync(CancellationToken cancellationToken)
{
var httpContext = _httpContextAccessor.HttpContext;
var user = httpContext?.User;
if (user?.Identity is not { IsAuthenticated: true })
{
return Task.FromResult<IReadOnlyList<string>>(Array.Empty<string>());
}
var roles = user.Claims
.Where(c => c.Type == ClaimTypes.Role || string.Equals(c.Type, "role", StringComparison.OrdinalIgnoreCase))
.Select(c => c.Value)
.Where(v => !string.IsNullOrWhiteSpace(v))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
return Task.FromResult<IReadOnlyList<string>>(roles);
}
public async Task<bool> HasPermissionAsync(string permission, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(permission))
{
return true;
}
var roles = await GetUserRolesAsync(cancellationToken);
if (roles.Count == 0)
{
return false;
}
foreach (var role in roles)
{
if (RolePermissionConfig.HasPermission(role, permission))
{
return true;
}
}
return false;
}
}

View File

@@ -31,5 +31,6 @@
<ProjectReference Include="..\Protobufs\BackOffice.BFF.Role.Protobuf\BackOffice.BFF.Role.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.UserRole.Protobuf\BackOffice.BFF.UserRole.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.Category.Protobuf\BackOffice.BFF.Category.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.ManualPayment.Protobuf\BackOffice.BFF.ManualPayment.Protobuf.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,79 @@
using System;
using System.Threading.Tasks;
using BackOffice.BFF.Application.Common.Interfaces;
using Grpc.AspNetCore.Server;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.WebApi.Common.Authorization;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class RequiresPermissionAttribute : Attribute
{
public RequiresPermissionAttribute(string permission)
{
Permission = permission ?? throw new ArgumentNullException(nameof(permission));
}
public string Permission { get; }
}
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);
}
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 for permission {Permission}", attribute.Permission);
throw new RpcException(new Status(StatusCode.PermissionDenied, "Permission denied"));
}
}
}
}

View File

@@ -9,3 +9,5 @@ global using Microsoft.AspNetCore.Builder;
global using System;
global using Microsoft.AspNetCore.Routing;
global using System.Linq;
global using BackOffice.BFF.WebApi.Common.Authorization;
global using BackOffice.BFF.Application.Common.Models;

View File

@@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
using Serilog;
using Serilog.Core;
using Microsoft.OpenApi.Models;
using BackOffice.BFF.WebApi.Common.Authorization;
var builder = WebApplication.CreateBuilder(args);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
@@ -37,6 +38,7 @@ builder.Services.AddGrpc(options =>
options.EnableDetailedErrors = true;
options.MaxReceiveMessageSize = 1000 * 1024 * 1024; // 1 GB
options.MaxSendMessageSize = 1000 * 1024 * 1024; // 1 GB
options.Interceptors.Add<PermissionInterceptor>();
}).AddJsonTranscoding();
builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddApplicationServices();

View File

@@ -21,6 +21,7 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
public override async Task<Empty> CreateOrUpdateConfiguration(
CreateOrUpdateConfigurationRequest request,
ServerCallContext context)
@@ -30,6 +31,7 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
context);
}
[RequiresPermission(PermissionNames.SettingsManageConfiguration)]
public override async Task<Empty> DeactivateConfiguration(
DeactivateConfigurationRequest request,
ServerCallContext context)
@@ -39,6 +41,7 @@ public class ConfigurationService : ConfigurationContract.ConfigurationContractB
context);
}
[RequiresPermission(PermissionNames.SettingsView)]
public override async Task<GetAllConfigurationsResponse> GetAllConfigurations(
GetAllConfigurationsRequest request,
ServerCallContext context)

View File

@@ -0,0 +1,76 @@
using BackOffice.BFF.ManualPayment.Protobuf;
using BackOffice.BFF.WebApi.Common.Services;
using BackOffice.BFF.Application.ManualPaymentCQ.Commands.CreateManualPayment;
using BackOffice.BFF.Application.ManualPaymentCQ.Commands.ApproveManualPayment;
using BackOffice.BFF.Application.ManualPaymentCQ.Commands.RejectManualPayment;
using BackOffice.BFF.Application.ManualPaymentCQ.Queries.GetManualPayments;
using Google.Protobuf.WellKnownTypes;
using Mapster;
using MediatR;
namespace BackOffice.BFF.WebApi.Services;
public class ManualPaymentService : ManualPaymentContract.ManualPaymentContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _sender;
public ManualPaymentService(
IDispatchRequestToCQRS dispatchRequestToCQRS,
ISender sender)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
_sender = sender;
}
[RequiresPermission(PermissionNames.ManualPaymentsCreate)]
public override async Task<CreateManualPaymentResponse> CreateManualPayment(
CreateManualPaymentRequest request,
ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateManualPaymentRequest, CreateManualPaymentCommand, CreateManualPaymentResponse>(
request,
context);
}
[RequiresPermission(PermissionNames.ManualPaymentsApprove)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> ApproveManualPayment(
ApproveManualPaymentRequest request,
ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ApproveManualPaymentRequest, ApproveManualPaymentCommand>(
request,
context);
}
[RequiresPermission(PermissionNames.ManualPaymentsApprove)]
public override async Task<Google.Protobuf.WellKnownTypes.Empty> RejectManualPayment(
RejectManualPaymentRequest request,
ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<RejectManualPaymentRequest, RejectManualPaymentCommand>(
request,
context);
}
[RequiresPermission(PermissionNames.ManualPaymentsView)]
public override async Task<GetManualPaymentsResponse> GetManualPayments(
GetManualPaymentsRequest request,
ServerCallContext context)
{
var query = new GetManualPaymentsQuery
{
PageNumber = request.PageNumber,
PageSize = request.PageSize,
UserId = request.UserId?.Value,
Status = request.Status?.Value,
Type = request.Type?.Value,
ReferenceNumber = request.ReferenceNumber?.Value,
// RequestedBy و OrderByDescending در این نسخه از UI ارسال نمی‌شود
};
var result = await _sender.Send(query, context.CancellationToken);
return result.Adapt<GetManualPaymentsResponse>();
}
}

View File

@@ -19,47 +19,62 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
[RequiresPermission(PermissionNames.OrdersCreate)]
public override async Task<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CreateNewUserOrderRequest, CreateNewUserOrderCommand, CreateNewUserOrderResponse>(request, context);
}
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<Empty> UpdateUserOrder(UpdateUserOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateUserOrderRequest, UpdateUserOrderCommand>(request, context);
}
[RequiresPermission(PermissionNames.OrdersDelete)]
public override async Task<Empty> DeleteUserOrder(DeleteUserOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<DeleteUserOrderRequest, DeleteUserOrderCommand>(request, context);
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<GetUserOrderResponse> GetUserOrder(GetUserOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetUserOrderRequest, GetUserOrderQuery, GetUserOrderResponse>(request, context);
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<GetAllUserOrderByFilterResponse> GetAllUserOrderByFilter(GetAllUserOrderByFilterRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAllUserOrderByFilterRequest, GetAllUserOrderByFilterQuery, GetAllUserOrderByFilterResponse>(request, context);
}
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<UpdateOrderStatusResponse> UpdateOrderStatus(UpdateOrderStatusRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<UpdateOrderStatusRequest, UpdateOrderStatusCommand, UpdateOrderStatusResponse>(request, context);
}
[RequiresPermission(PermissionNames.ReportsView)]
public override async Task<GetOrdersByDateRangeResponse> GetOrdersByDateRange(GetOrdersByDateRangeRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetOrdersByDateRangeRequest, GetOrdersByDateRangeQuery, GetOrdersByDateRangeResponse>(request, context);
}
[RequiresPermission(PermissionNames.OrdersUpdate)]
public override async Task<ApplyDiscountToOrderResponse> ApplyDiscountToOrder(ApplyDiscountToOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<ApplyDiscountToOrderRequest, ApplyDiscountToOrderCommand, ApplyDiscountToOrderResponse>(request, context);
}
[RequiresPermission(PermissionNames.OrdersView)]
public override async Task<CalculateOrderPVResponse> CalculateOrderPV(CalculateOrderPVRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CalculateOrderPVRequest, CalculateOrderPVQuery, CalculateOrderPVResponse>(request, context);
}
[RequiresPermission(PermissionNames.OrdersCancel)]
public override async Task<CancelOrderResponse> CancelOrder(CancelOrderRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<CancelOrderRequest, CancelOrderCommand, CancelOrderResponse>(request, context);

View File

@@ -41,6 +41,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackOffice.BFF.Commission.P
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackOffice.BFF.Common.Protobuf", "Protobufs\BackOffice.BFF.Common.Protobuf\BackOffice.BFF.Common.Protobuf.csproj", "{9911D6BE-3022-44F6-B93B-B3D62A14FBCA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackOffice.BFF.ManualPayment.Protobuf", "Protobufs\BackOffice.BFF.ManualPayment.Protobuf\BackOffice.BFF.ManualPayment.Protobuf.csproj", "{389D8C44-E796-41EE-BBF2-7A058735EA50}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -267,6 +269,18 @@ Global
{9911D6BE-3022-44F6-B93B-B3D62A14FBCA}.Release|x64.Build.0 = Release|Any CPU
{9911D6BE-3022-44F6-B93B-B3D62A14FBCA}.Release|x86.ActiveCfg = Release|Any CPU
{9911D6BE-3022-44F6-B93B-B3D62A14FBCA}.Release|x86.Build.0 = Release|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Debug|x64.ActiveCfg = Debug|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Debug|x64.Build.0 = Debug|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Debug|x86.ActiveCfg = Debug|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Debug|x86.Build.0 = Debug|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Release|Any CPU.Build.0 = Release|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Release|x64.ActiveCfg = Release|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Release|x64.Build.0 = Release|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Release|x86.ActiveCfg = Release|Any CPU
{389D8C44-E796-41EE-BBF2-7A058735EA50}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -286,6 +300,7 @@ Global
{CA0F6C82-227A-41E4-A59F-B45EF68411A1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{3B7514DE-1C2F-4BB1-BBD5-C57BEEC6843E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{9911D6BE-3022-44F6-B93B-B3D62A14FBCA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{389D8C44-E796-41EE-BBF2-7A058735EA50} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0AE1AB4A-3C91-4853-93C2-C2476E79F845}

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.1</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<PackageId>Foursat.BackOffice.BFF.ManualPayment.Protobuf</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.23.3" />
<PackageReference Include="Grpc.Core.Api" Version="2.54.0" />
<PackageReference Include="Grpc.Tools" Version="2.72.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.10.0" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\manualpayment.proto"
ProtoRoot="Protos\"
GrpcServices="Both"
AdditionalImportDirs="..\BackOffice.BFF.Common.Protobuf\Protos" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BackOffice.BFF.Common.Protobuf\BackOffice.BFF.Common.Protobuf.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,84 @@
syntax = "proto3";
package manualpayment;
import "public_messages.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "BackOffice.BFF.ManualPayment.Protobuf";
service ManualPaymentContract
{
rpc CreateManualPayment(CreateManualPaymentRequest) returns (CreateManualPaymentResponse);
rpc ApproveManualPayment(ApproveManualPaymentRequest) returns (google.protobuf.Empty);
rpc RejectManualPayment(RejectManualPaymentRequest) returns (google.protobuf.Empty);
rpc GetManualPayments(GetManualPaymentsRequest) returns (GetManualPaymentsResponse);
}
message GetManualPaymentsRequest
{
int32 page_number = 1;
int32 page_size = 2;
google.protobuf.Int64Value user_id = 3;
google.protobuf.Int32Value status = 4;
google.protobuf.Int32Value type = 5;
google.protobuf.StringValue reference_number = 6;
}
message GetManualPaymentsResponse
{
messages.MetaData meta_data = 1;
repeated ManualPaymentModel models = 2;
}
message ManualPaymentModel
{
int64 id = 1;
int64 user_id = 2;
string user_full_name = 3;
string user_mobile = 4;
int64 amount = 5;
int32 type = 6;
string type_display = 7;
string description = 8;
string reference_number = 9;
int32 status = 10;
string status_display = 11;
int64 requested_by = 12;
string requested_by_name = 13;
google.protobuf.Int64Value approved_by = 14;
google.protobuf.StringValue approved_by_name = 15;
google.protobuf.Timestamp approved_at = 16;
string rejection_reason = 17;
google.protobuf.Int64Value transaction_id = 18;
google.protobuf.Timestamp created = 19;
}
message CreateManualPaymentRequest
{
int64 user_id = 1;
int64 amount = 2;
int32 type = 3;
string description = 4;
google.protobuf.StringValue reference_number = 5;
}
message CreateManualPaymentResponse
{
int64 id = 1;
}
message ApproveManualPaymentRequest
{
int64 manual_payment_id = 1;
google.protobuf.StringValue approval_note = 2;
}
message RejectManualPaymentRequest
{
int64 manual_payment_id = 1;
string rejection_reason = 2;
}

View File

@@ -0,0 +1,70 @@
syntax = "proto3";
package messages;
option csharp_namespace = "CMSMicroservice.Protobuf.Protos";
service PublicMessageContract{}
message PaginationState
{
int32 page_number = 1;
int32 page_size = 2;
}
message MetaData
{
int64 current_page = 1;
int64 total_page = 2;
int64 page_size = 3;
int64 total_count = 4;
bool has_previous = 5;
bool has_next = 6;
}
message DecimalValue
{
int64 units = 1;
sfixed32 nanos = 2;
}
enum PaymentStatus
{
Success = 0;
Reject = 1;
Pending = 2;
}
// وضعیت ارسال سفارش
enum DeliveryStatus
{
// نامشخص / نیاز به ارسال ندارد (مثلا سفارش پکیج)
DeliveryStatus_None = 0;
// ثبت شده و در انتظار آماده‌سازی/ارسال
DeliveryStatus_Pending = 1;
// تحویل پست/حمل‌ونقل شده است
DeliveryStatus_InTransit = 2;
// توسط مشتری دریافت شده است
DeliveryStatus_Delivered = 3;
// مرجوع شده
DeliveryStatus_Returned = 4;
}
enum TransactionType
{
Buy = 0;
DepositIpg = 1;
DepositExternal1 = 2;
Withdraw = 3;
}
enum ContractType
{
Main = 0;
CMS = 1;
}
enum PaymentMethod
{
IPG = 0;
Wallet = 1;
}

View File

@@ -165,6 +165,11 @@ message GetUserOrderResponse
{
PaymentMethod payment_method = 16;
}
// اطلاعات مالیات بر ارزش افزوده
int64 vat_amount = 17;
double vat_percentage = 18;
int64 vat_base_amount = 19;
int64 vat_total_amount = 20;
}
message GetAllUserOrderByFilterRequest
{
@@ -226,6 +231,9 @@ message GetAllUserOrderByFilterResponseModel
{
PaymentMethod payment_method = 15;
}
// مبلغ و درصد مالیات بر ارزش افزوده
int64 vat_amount = 16;
double vat_percentage = 17;
}
// جزئیات فاکتور سفارش