Compare commits

...

49 Commits

Author SHA1 Message Date
masoodafar-web 28e81041fc chore: Update production workflow to use Nexus registry (no proxy needed)
Build and Deploy to Production / build-and-deploy (push) Failing after 0s
- Changed Dockerfile base images to use 194.5.195.53:32082 (Nexus)
- Updated workflow to use docker-sshpass image from Nexus
- Removed HTTP_PROXY/HTTPS_PROXY dependencies
- Changed deploy method to SSH instead of kubeconfig
- Registry: 194.5.195.53:30080 (Gitea internal)
- K8S Server: 45.149.79.127 (Production)
2026-01-29 22:04:31 +03:30
masoodafar-web ecac62d095 Merge branch 'kub-stage' into production
Build and Deploy to Production / build-and-deploy (push) Successful in 1m42s
2025-12-29 00:40:34 +03:30
masoodafar-web a8a3d246da feat: enhance WeeklyBalanceModel with carryover and new members fields; update mappings and response DTO
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m55s
2025-12-28 23:57:26 +03:30
masoodafar-web 0ed694f475 fix: ensure WeekDefinitionId is correctly mapped to nullable type in GetMyWeeklyBalancesQuery
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m39s
2025-12-27 22:07:35 +03:30
masoodafar-web 99c0da4ecc feat: add production configuration for JWT and gRPC settings
Build and Deploy to Production / build-and-deploy (push) Successful in 2m31s
2025-12-27 07:42:45 +03:30
masoodafar-web 0bab1d857e refactor: streamline mapping for GetMyWeeklyBalancesRequest to GetMyWeeklyBalancesQuery
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m31s
2025-12-27 07:35:49 +03:30
masoodafar-web 84c519060f refactor: simplify status handling in commission payout queries and responses
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m34s
2025-12-27 07:10:44 +03:30
masoodafar-web a429a2328a fix: remove unnecessary type parameter from WithdrawBalance handling
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m31s
2025-12-27 06:22:28 +03:30
masoodafar-web c53056c750 fix: correct formatting of PYMSMSAddress in appsettings.json
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m35s
2025-12-27 03:49:48 +03:30
masoodafar-web dadd0ee66c fix: update CMSMSAddress to use secure HTTPS URL 2025-12-27 03:46:12 +03:30
masoodafar-web b71d948e0e feat: add ReferralCode to network tree queries and responses
Build and Deploy to Production / build-and-deploy (push) Successful in 1m42s
2025-12-26 03:52:15 +03:30
masoodafar-web a189340aac feat(AppVersion): Add BFF handler for AppVersion
Build and Deploy to Production / build-and-deploy (push) Successful in 2m49s
- Add GetAppVersionQuery and Handler in Application layer
- Add AppVersionGrpcService in WebApi
- Add AppVersion to IApplicationContractContext and ApplicationContractContext
- Update CMSMicroservice.Protobuf to 0.0.158
2025-12-25 19:07:16 +03:30
masoodafar-web e35ad0ebfb fix: update CMSMSAddress in appsettings for correct service URL
Build and Deploy to Production / build-and-deploy (push) Successful in 1m46s
2025-12-25 18:50:53 +03:30
masoodafar-web 0364749342 Merge branch 'stage-new' into production 2025-12-25 18:49:59 +03:30
masoodafar-web 07e420de1b feat(AppVersion): Add AppVersion proto for cache invalidation 2025-12-25 18:42:50 +03:30
admin f471ee468a fix: correct dockerfile path and registry
Build and Deploy to Production / build-and-deploy (push) Successful in 1m37s
2025-12-23 22:33:13 +00:00
admin 3f22628bf4 fix: correct dockerfile path and registry
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2025-12-23 22:33:05 +00:00
admin 452544b527 feat: add production deployment workflow
Build and Deploy to Production / build-and-deploy (push) Failing after 11s
2025-12-23 22:19:48 +00:00
admin 47c0bcd76f fix: update appsettings for production K8s 2025-12-23 22:17:07 +00:00
admin 3e3033386d fix: update appsettings for production K8s 2025-12-23 22:16:37 +00:00
masoodafar-web b7057e0928 fix: update CMS and Seq service URLs to production endpoints 2025-12-20 05:20:11 +03:30
masoodafar-web 97b62e9168 fix: correct branch name in deployment configuration from 'stage_new' to 'stage-new' 2025-12-20 05:04:09 +03:30
masoodafar-web 2a2599973e fix: update branch name in deployment configuration from 'stage' to 'stage_new' 2025-12-20 05:01:24 +03:30
masoodafar-web 2d2840485f feat: update version to 0.0.3 and add target for pushing NuGet package to FourSat 2025-12-20 04:59:36 +03:30
masoodafar-web 954c7fe5c4 fix: correct formatting of PYMSMSAddress in appsettings.json
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m38s
2025-12-20 03:47:50 +03:30
masoodafar-web 565a9605dc feat: add GetWeekDefinitions RPC and corresponding query handler for week retrieval 2025-12-20 03:30:22 +03:30
masoodafar-web 0cca79ebfe feat: add Configuration gRPC service and queries for club settings and features 2025-12-19 00:30:58 +03:30
masoodafar-web 9db854ba0e feat: add GetVATRate RPC and corresponding query handler for VAT rate retrieval 2025-12-18 03:50:00 +03:30
masoodafar-web 5c03335f0b feat: add subordinate network tree query with security checks 2025-12-18 03:14:37 +03:30
masoodafar-web 4330ec3726 feat: add club membership contract signing and city services 2025-12-18 00:44:44 +03:30
masoodafar-web 63f05e0883 feat: add base package payment initiation and verification 2025-12-16 02:34:22 +03:30
masoodafar-web b4e41163ec fix: correct CMSMS service address typo 2025-12-08 03:09:22 +03:30
masoodafar-web 56c607833f Merge branch 'kub-stage' of http://194.5.195.53:30080/admin/FrontOffice.BFF into kub-stage
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 2m2s
2025-12-08 01:44:46 +03:30
masoodafar-web 61f48857aa feat: update protobuf project versions and refactor push command 2025-12-08 01:31:46 +03:30
masoud 2dad5e46e8 fix: update kubeconfig for deployment (127.0.0.1 → 194.5.195.53)
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 3m49s
2025-12-07 19:17:26 +00:00
masoud eb4beeebce chore: test multi-remote push to git.afrino.co
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 2m6s
2025-12-07 18:52:57 +00:00
masoud 265004df90 chore: trigger rebuild with new registry domain
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 2m12s
2025-12-07 17:32:26 +00:00
masoud 8ddf33dfb7 chore: migrate from IP to domain (git.foursat.afrino.co)
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2025-12-07 17:01:59 +00:00
masoodafar-web 663b357d10 fix: Update deployment name case in kub-deploy.yml
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2025-12-06 23:40:45 +03:30
masoodafar-web b51a24d307 feat: Update CMSMS address in appsettings 2025-12-06 23:32:42 +03:30
masoodafar-web a47be9f585 feat: Update port, enable console logging, upgrade protobuf version 2025-12-06 23:30:26 +03:30
masoodafar-web e651e5a292 Merge remote-tracking branch 'kub-stage/kub-stage' into kub-stage
# Conflicts:
#	.gitea/workflows/kub-deploy.yml
#	.github/workflows/kub-deploy.yml
#	src/FrontOffice.BFF.WebApi/Dockerfile
2025-12-06 23:23:55 +03:30
masoodafar-web ce091044c0 feat: Add CI/CD workflows for stage and Kubernetes deployment 2025-12-06 23:23:24 +03:30
masoodafar-web 6e70031691 Test auto-deploy 2025-12-06 23:21:34 +03:30
masoodafar-web 13037e9533 Merge branch 'main' into kub-stage 2025-12-06 23:10:53 +03:30
masoodafar-web d7ae666468 Add Dockerfile and NuGet config for containerization 2025-12-06 23:10:07 +03:30
masoud 609f79723f Use CMS workflow pattern with insecure registry config 2025-12-06 17:21:47 +00:00
masoud 07f7819820 Add insecure-registries config for Docker daemon 2025-12-06 17:14:50 +00:00
masoodafar-web 13e33e7b1f feat: Add VAT information to GetUserOrderResponse and OrderVATInfo message 2025-12-05 17:27:45 +03:30
114 changed files with 3977 additions and 217 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ name: Push nuget and docker image Actions Workflow
on: on:
push: push:
branches: branches:
- stage - stage-new
jobs: jobs:
Deploy: Deploy:
runs-on: windows runs-on: windows
+17
View File
@@ -0,0 +1,17 @@
name: Push nuget and docker image Actions Workflow
on:
push:
branches:
- stage
jobs:
Deploy:
runs-on: windows
steps:
- name: Checkout
uses: https://git.afrino.co/actions/checkout@v3
- name: Setup dotnet
uses: https://git.afrino.co/actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
+51 -10
View File
@@ -1,4 +1,4 @@
name: Build and Deploy name: Build and Deploy to Kubernetes
on: on:
push: push:
@@ -6,33 +6,60 @@ on:
- kub-stage - kub-stage
env: env:
REGISTRY: 194.5.195.53:30080 REGISTRY: gitea-svc:3000
IMAGE_NAME: admin/frontoffice-bff IMAGE_NAME: admin/frontoffice-bff
jobs: jobs:
build: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: docker:latest image: docker:latest
options: --privileged options: --privileged
env:
HTTP_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
HTTPS_PROXY: http://proxyuser:87zH26nbqT2@46.249.98.211:3128
NO_PROXY: localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8
steps: steps:
- name: Install git - name: Install dependencies
run: | run: |
apk add --no-cache git apk add --no-cache git curl
- name: Checkout code # Install kubectl
run: | curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
git clone --depth 1 --branch kub-stage http://194.5.195.53:30080/admin/FrontOffice.BFF.git . chmod +x kubectl
git log -1 --format="%H %s" mv kubectl /usr/local/bin/
- name: Start Docker daemon - name: Start Docker daemon with insecure registry
run: | run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["git.foursat.afrino.co", "gitea-svc:3000"]
}
DAEMON
mkdir -p ~/.docker
cat > ~/.docker/config.json << 'CONF'
{
"proxies": {
"default": {
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
"noProxy": "localhost,127.0.0.1,gitea-svc,194.5.195.53,10.0.0.0/8"
}
}
}
CONF
dockerd & dockerd &
for i in $(seq 1 30); do for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && break || sleep 2 docker info >/dev/null 2>&1 && break || sleep 2
done done
docker info docker info
- name: Checkout code
run: |
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/FrontOffice.BFF.git .
git log -1 --format="%H %s"
- name: Build Docker Image - name: Build Docker Image
run: | run: |
cd src cd src
@@ -43,8 +70,22 @@ jobs:
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \ --build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
. .
- name: Push to Registry - name: Push to Registry
run: | run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- name: Deploy to Kubernetes
run: |
# Setup kubeconfig
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > ~/.kube/config
# Restart deployment to pull new image
kubectl rollout restart deployment/frontoffice-bff || echo "Deployment doesn't exist yet"
# Wait for rollout to complete
kubectl rollout status deployment/frontoffice-bff --timeout=5m || echo "Deployment rollout pending"
+68
View File
@@ -0,0 +1,68 @@
name: Build and Deploy to Production
on:
push:
branches:
- production
env:
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/frontoffice-bff
K8S_SERVER: 45.149.79.127
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: 194.5.195.53:32082/docker-sshpass:latest
options: --privileged
steps:
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32082"]
}
DAEMON
echo "🚀 Starting Docker daemon..."
dockerd &
for i in $(seq 1 90); do
if docker info >/dev/null 2>&1; then
echo "✅ Docker daemon is ready (attempt $i)"
docker version
break
else
echo "⏳ Waiting for Docker daemon... (attempt $i/90)"
sleep 2
fi
done
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start"
exit 1
fi
- name: Checkout code
run: |
git clone --depth 1 --branch production http://gitea-svc:3000/admin/FrontOffice.BFF.git .
- name: Build Docker Image
run: |
cd src
docker build -f FrontOffice.BFF.WebApi/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
.
- name: Push to Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod
- name: Deploy to Production
run: |
sshpass -p "${{ secrets.K8S_SSH_PASSWORD }}" ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} \
"kubectl rollout restart deployment/frontoffice-bff && kubectl rollout status deployment/frontoffice-bff --timeout=5m" || echo "Deployment pending"
+1 -1
View File
@@ -492,5 +492,5 @@ fabric.properties
.idea/caches/build_file_checksums.ser .idea/caches/build_file_checksums.ser
/src/.idea /src/.idea
/.gitea
src/.dockerignore src/.dockerignore
@@ -0,0 +1,53 @@
using MediatR;
namespace FrontOffice.BFF.Application.CityCQ.Queries.GetAllCitiesByFilter;
/// <summary>
/// Query برای دریافت لیست شهرها با فیلتر و صفحه‌بندی
/// </summary>
public sealed record GetAllCitiesByFilterQuery : IRequest<GetAllCitiesByFilterResponseDto>
{
/// <summary>
/// موقعیت صفحه بندی
/// </summary>
public PaginationStateDto? PaginationState { get; init; }
/// <summary>
/// مرتب سازی بر اساس
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// فیلتر
/// </summary>
public GetAllCitiesByFilterFilterDto? Filter { get; init; }
}
public class PaginationStateDto
{
public int PageNumber { get; set; }
public int PageSize { get; set; }
}
public class GetAllCitiesByFilterFilterDto
{
/// <summary>
/// شناسه
/// </summary>
public long? Id { get; set; }
/// <summary>
/// نام شهر (Contains)
/// </summary>
public string? Name { get; set; }
/// <summary>
/// نام بومی شهر (Contains)
/// </summary>
public string? Native { get; set; }
/// <summary>
/// شناسه استان
/// </summary>
public long? StateId { get; set; }
}
@@ -0,0 +1,25 @@
using CMSMicroservice.Protobuf.Protos.City;
using FrontOffice.BFF.Application.Common.Interfaces;
using Mapster;
using MediatR;
namespace FrontOffice.BFF.Application.CityCQ.Queries.GetAllCitiesByFilter;
public class GetAllCitiesByFilterQueryHandler : IRequestHandler<GetAllCitiesByFilterQuery, GetAllCitiesByFilterResponseDto>
{
private readonly IApplicationContractContext _context;
public GetAllCitiesByFilterQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAllCitiesByFilterResponseDto> Handle(
GetAllCitiesByFilterQuery request,
CancellationToken cancellationToken)
{
var grpcRequest = request.Adapt<GetAllCitiesByFilterRequest>();
var response = await _context.Cities.GetAllCitiesByFilterAsync(grpcRequest, cancellationToken: cancellationToken);
return response.Adapt<GetAllCitiesByFilterResponseDto>();
}
}
@@ -0,0 +1,72 @@
namespace FrontOffice.BFF.Application.CityCQ.Queries.GetAllCitiesByFilter;
public class GetAllCitiesByFilterResponseDto
{
/// <summary>
/// متادیتا صفحه‌بندی
/// </summary>
public MetaDataDto MetaData { get; set; } = null!;
/// <summary>
/// لیست شهرها
/// </summary>
public List<CityDto> Models { get; set; } = new();
}
public class MetaDataDto
{
public long CurrentPage { get; set; }
public long TotalPage { get; set; }
public long PageSize { get; set; }
public long TotalCount { get; set; }
public bool HasPrevious { get; set; }
public bool HasNext { get; set; }
}
public class CityDto
{
/// <summary>
/// شناسه
/// </summary>
public long Id { get; set; }
/// <summary>
/// شناسه خارجی
/// </summary>
public long ExternalId { get; set; }
/// <summary>
/// نام شهر (انگلیسی)
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// نام بومی شهر (فارسی)
/// </summary>
public string Native { get; set; } = null!;
/// <summary>
/// عرض جغرافیایی
/// </summary>
public string Latitude { get; set; } = null!;
/// <summary>
/// طول جغرافیایی
/// </summary>
public string Longitude { get; set; } = null!;
/// <summary>
/// شناسه استان
/// </summary>
public long StateId { get; set; }
/// <summary>
/// نام استان
/// </summary>
public string StateName { get; set; } = null!;
/// <summary>
/// نام بومی استان (فارسی)
/// </summary>
public string StateNative { get; set; } = null!;
}
@@ -0,0 +1,33 @@
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract;
/// <summary>
/// Command برای امضای قرارداد باشگاه مشتریان
/// </summary>
public record AcceptClubMembershipContractCommand : IRequest<AcceptClubMembershipContractResponseDto>
{
/// <summary>
/// کد OTP دریافتی
/// </summary>
public string OtpCode { get; init; }
/// <summary>
/// شناسه یکتای امضا (GUID)
/// </summary>
public string SignGuid { get; init; }
/// <summary>
/// محتوای HTML قرارداد
/// </summary>
public string ContractHtml { get; init; }
}
/// <summary>
/// DTO پاسخ امضای قرارداد
/// </summary>
public class AcceptClubMembershipContractResponseDto
{
public bool Success { get; set; }
public string Message { get; set; }
public long ContractId { get; set; }
public string Token { get; set; }
}
@@ -0,0 +1,80 @@
using CMSMicroservice.Protobuf.Protos.ClubMembership;
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract;
/// <summary>
/// Handler برای امضای قرارداد باشگاه مشتریان
/// 1. فراخوانی CMS برای ثبت قرارداد و فعالسازی باشگاه
/// 2. دریافت توکن جدید با claims به‌روز شده (IsClubMemberActive = true)
/// </summary>
public class AcceptClubMembershipContractCommandHandler
: IRequestHandler<AcceptClubMembershipContractCommand, AcceptClubMembershipContractResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public AcceptClubMembershipContractCommandHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<AcceptClubMembershipContractResponseDto> Handle(
AcceptClubMembershipContractCommand request,
CancellationToken cancellationToken)
{
var userId = _currentUserService.UserId
?? throw new ForbiddenAccessException();
// 1. فراخوانی CMS برای ثبت قرارداد و فعالسازی باشگاه
var cmsResponse = await _context.ClubMemberships.AcceptClubMembershipContractAsync(
new AcceptClubMembershipContractRequest
{
UserId = userId,
OtpCode = request.OtpCode,
SignGuid = request.SignGuid,
ContractHtml = request.ContractHtml
},
cancellationToken: cancellationToken);
if (!cmsResponse.Success)
{
return new AcceptClubMembershipContractResponseDto
{
Success = false,
Message = cmsResponse.Message,
ContractId = 0,
Token = null
};
}
// 2. دریافت توکن جدید با claims به‌روز شده
string newToken = null;
try
{
var tokenResponse = await _context.User.GetJwtTokenAsync(
new CMSMicroservice.Protobuf.Protos.User.GetJwtTokenRequest
{
Id = userId
},
cancellationToken: cancellationToken);
newToken = tokenResponse?.Token;
}
catch
{
// اگر دریافت توکن با خطا مواجه شد، عملیات اصلی موفق بوده
// کاربر می‌تواند با login مجدد توکن جدید بگیرد
}
return new AcceptClubMembershipContractResponseDto
{
Success = true,
Message = cmsResponse.Message,
ContractId = cmsResponse.ContractId,
Token = newToken
};
}
}
@@ -0,0 +1,21 @@
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract;
public class AcceptClubMembershipContractCommandValidator : AbstractValidator<AcceptClubMembershipContractCommand>
{
public AcceptClubMembershipContractCommandValidator()
{
RuleFor(x => x.OtpCode)
.NotEmpty()
.WithMessage("کد تایید الزامی است")
.Length(6)
.WithMessage("کد تایید باید ۶ رقم باشد");
RuleFor(x => x.SignGuid)
.NotEmpty()
.WithMessage("شناسه امضا الزامی است");
RuleFor(x => x.ContractHtml)
.NotEmpty()
.WithMessage("محتوای قرارداد الزامی است");
}
}
@@ -0,0 +1,23 @@
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.RequestClubContractOtp;
/// <summary>
/// Command برای درخواست OTP امضای قرارداد باشگاه مشتریان
/// </summary>
public record RequestClubContractOtpCommand : IRequest<RequestClubContractOtpResponseDto>
{
/// <summary>
/// شناسه یکتای امضا (GUID)
/// </summary>
public string SignGuid { get; init; }
}
/// <summary>
/// DTO پاسخ درخواست OTP
/// </summary>
public class RequestClubContractOtpResponseDto
{
public bool Success { get; set; }
public string Message { get; set; }
public int RemainingAttempts { get; set; }
public int RemainingSeconds { get; set; }
}
@@ -0,0 +1,80 @@
using System.Text;
using CMSMicroservice.Protobuf.Protos.OtpToken;
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.RequestClubContractOtp;
/// <summary>
/// Handler برای درخواست OTP امضای قرارداد باشگاه مشتریان
/// از سرویس OTP موجود در CMS استفاده می‌کند و پیامک ارسال می‌کند
/// </summary>
public class RequestClubContractOtpCommandHandler
: IRequestHandler<RequestClubContractOtpCommand, RequestClubContractOtpResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly IKavenegarService _kavenegarService;
private readonly ICurrentUserService _currentUserService;
private const string OtpPurpose = "signClubContract";
public RequestClubContractOtpCommandHandler(
IApplicationContractContext context,
IKavenegarService kavenegarService,
ICurrentUserService currentUserService)
{
_context = context;
_kavenegarService = kavenegarService;
_currentUserService = currentUserService;
}
public async Task<RequestClubContractOtpResponseDto> Handle(
RequestClubContractOtpCommand request,
CancellationToken cancellationToken)
{
// دریافت شماره موبایل از توکن کاربر
var mobileNumber = _currentUserService.MobileNumber;
if (string.IsNullOrEmpty(mobileNumber))
{
return new RequestClubContractOtpResponseDto
{
Success = false,
Message = "شماره موبایل کاربر یافت نشد"
};
}
// فراخوانی سرویس OTP در CMS
var otpResponse = await _context.OtpToken.CreateNewOtpTokenAsync(
new CreateNewOtpTokenRequest
{
Mobile = mobileNumber,
Purpose = OtpPurpose
},
cancellationToken: cancellationToken);
// ارسال پیامک با کد OTP
if (otpResponse.Success && !string.IsNullOrWhiteSpace(otpResponse.Code))
{
var fullName = $"{_currentUserService.FirstName} {_currentUserService.LastName}".Trim();
await _kavenegarService.Send(
mobile: mobileNumber,
new StringBuilder("سلام ")
.Append(string.IsNullOrEmpty(fullName) ? "کاربر" : fullName)
.AppendLine(" عزیز")
.Append("کد یک بار مصرف برای تایید قرارداد باشگاه مشتریان: ")
.AppendLine(otpResponse.Code)
.AppendLine("شناسه امضاء: ")
.AppendLine(request.SignGuid)
.AppendLine("کارابازار")
.ToString());
}
return new RequestClubContractOtpResponseDto
{
Success = otpResponse.Success,
Message = otpResponse.Message,
RemainingAttempts = otpResponse.RemainingAttempts,
RemainingSeconds = otpResponse.RemainingSeconds
};
}
}
@@ -0,0 +1,11 @@
namespace FrontOffice.BFF.Application.ClubMembershipCQ.Commands.RequestClubContractOtp;
public class RequestClubContractOtpCommandValidator : AbstractValidator<RequestClubContractOtpCommand>
{
public RequestClubContractOtpCommandValidator()
{
RuleFor(x => x.SignGuid)
.NotEmpty()
.WithMessage("شناسه امضا الزامی است");
}
}
@@ -6,9 +6,9 @@ namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayout
public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto> public record GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto>
{ {
/// <summary> /// <summary>
/// شماره هفته در فرمت ISO (مثال: "2025-W48"، null = همه) /// شناسه تعریف هفته (null = همه)
/// </summary> /// </summary>
public string? WeekNumber { get; init; } public long? WeekDefinitionId { get; init; }
/// <summary> /// <summary>
/// وضعیت: 0=Pending, 1=Calculated, 2=Paid, 3=Withdrawn (null = همه) /// وضعیت: 0=Pending, 1=Calculated, 2=Paid, 3=Withdrawn (null = همه)
@@ -28,8 +28,8 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
}; };
// WeekNumber is string type in proto - assign directly // WeekNumber is string type in proto - assign directly
if (!string.IsNullOrEmpty(request.WeekNumber)) if (request.WeekDefinitionId!=null)
cmsRequest.WeekNumber = request.WeekNumber; cmsRequest.WeekDefinitionId = request.WeekDefinitionId;
if (request.Status.HasValue) if (request.Status.HasValue)
cmsRequest.Status = request.Status.Value; // int? type in proto cmsRequest.Status = request.Status.Value; // int? type in proto
@@ -39,13 +39,12 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
var payouts = response.Models.Select(p => new CommissionPayoutDto var payouts = response.Models.Select(p => new CommissionPayoutDto
{ {
Id = p.Id, Id = p.Id,
WeekNumber = p.WeekNumber, WeekDefinitionId = p.WeekDefinitionId,
WeekLabel = $"هفته {p.WeekNumber}", WeekDisplayName = p.WeekDisplayName,
BalancesEarned = p.BalancesEarned, BalancesEarned = (int)p.BalancesEarned,
TotalAmount = p.TotalAmount, TotalAmount = p.TotalAmount,
AmountFormatted = FormatCurrency(p.TotalAmount), AmountFormatted = FormatCurrency(p.TotalAmount),
Status = MapStatus(p.Status), Status = p.Status,
StatusBadgeColor = GetStatusColor(p.Status),
CalculatedDate = p.Created?.ToDateTime() ?? DateTime.UtcNow, CalculatedDate = p.Created?.ToDateTime() ?? DateTime.UtcNow,
DatePersian = FormatPersianDate(p.Created?.ToDateTime()) DatePersian = FormatPersianDate(p.Created?.ToDateTime())
}).ToList(); }).ToList();
@@ -59,30 +58,6 @@ public class GetMyCommissionPayoutsQueryHandler : IRequestHandler<GetMyCommissio
}; };
} }
private static string MapStatus(int status)
{
return status switch
{
0 => "Pending",
1 => "Calculated",
2 => "Paid",
3 => "Withdrawn",
_ => "Unknown"
};
}
private static string GetStatusColor(int status)
{
return status switch
{
0 => "warning",
1 => "info",
2 => "success",
3 => "success",
_ => "default"
};
}
private static string FormatCurrency(long amount) private static string FormatCurrency(long amount)
{ {
return $"{amount:N0} تومان"; return $"{amount:N0} تومان";
@@ -16,14 +16,14 @@ public class CommissionPayoutDto
public long Id { get; set; } public long Id { get; set; }
/// <summary> /// <summary>
/// شماره هفته (e.g., "2024-W45") /// شناسه تعریف هفته
/// </summary> /// </summary>
public string WeekNumber { get; set; } = string.Empty; public long WeekDefinitionId { get; set; }
/// <summary> /// <summary>
/// لیبل هفته (هفته 45 - آذر 1403) /// نام نمایشی هفته (هفته 45 - آذر 1403)
/// </summary> /// </summary>
public string WeekLabel { get; set; } = string.Empty; public string WeekDisplayName { get; set; } = string.Empty;
/// <summary> /// <summary>
/// تعداد Balance کسب شده /// تعداد Balance کسب شده
@@ -41,14 +41,9 @@ public class CommissionPayoutDto
public string AmountFormatted { get; set; } = string.Empty; public string AmountFormatted { get; set; } = string.Empty;
/// <summary> /// <summary>
/// وضعیت (Pending/Calculated/Paid/Withdrawn) /// وضعیت (0=Pending=1,Paid=2,WithdrawRequested=3,Withdrawn=4,PaymentFailed= 5,Cancelled)
/// </summary> /// </summary>
public string Status { get; set; } = string.Empty; public int Status { get; set; }
/// <summary>
/// رنگ Badge
/// </summary>
public string StatusBadgeColor { get; set; } = string.Empty;
/// <summary> /// <summary>
/// تاریخ محاسبه /// تاریخ محاسبه
@@ -6,9 +6,9 @@ namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
public record GetMyWeeklyBalancesQuery : IRequest<GetMyWeeklyBalancesResponseDto> public record GetMyWeeklyBalancesQuery : IRequest<GetMyWeeklyBalancesResponseDto>
{ {
/// <summary> /// <summary>
/// شماره هفته در فرمت ISO (مثال: "2025-W48"، null = هفته جاری) /// شناسه تعریف هفته (null = همه هفته‌ها)
/// </summary> /// </summary>
public string? WeekNumber { get; init; } public long? WeekDefinitionId { get; init; }
/// <summary> /// <summary>
/// فقط تعادل‌های فعال (غیر منقضی) /// فقط تعادل‌های فعال (غیر منقضی)
@@ -29,22 +29,29 @@ public class GetMyWeeklyBalancesQueryHandler : IRequestHandler<GetMyWeeklyBalanc
}; };
// WeekNumber is string in proto (format: "YYYY-Www") // WeekNumber is string in proto (format: "YYYY-Www")
if (!string.IsNullOrEmpty(request.WeekNumber)) if (request.WeekDefinitionId!=null && request.WeekDefinitionId>0)
cmsRequest.WeekNumber = request.WeekNumber; cmsRequest.WeekDefinitionId = request.WeekDefinitionId;
var response = await _context.Commission.GetUserWeeklyBalancesAsync(cmsRequest, cancellationToken: cancellationToken); var response = await _context.Commission.GetUserWeeklyBalancesAsync(cmsRequest, cancellationToken: cancellationToken);
// Map list of UserWeeklyBalanceModel to DTO // Map list of UserWeeklyBalanceModel to DTO
// Note: CMS proto uses LeftLegTotal/RightLegTotal (sum of NewMembers + Carryover)
var balances = response.Models.Select(b => new WeeklyBalanceItemDto var balances = response.Models.Select(b => new WeeklyBalanceItemDto
{ {
Id = b.Id, Id = b.Id,
WeekNumber = b.WeekNumber, WeekDefinitionId = b.WeekDefinitionId,
LeftLegBalances = b.LeftLegBalances, WeekLabel = b.WeekDisplayName,
RightLegBalances = b.RightLegBalances, LeftLegBalances = b.LeftLegTotal, // Changed from LeftLegBalances
RightLegBalances = b.RightLegTotal, // Changed from RightLegBalances
TotalBalances = b.TotalBalances, TotalBalances = b.TotalBalances,
WeeklyPoolContribution = b.WeeklyPoolContribution, WeeklyPoolContribution = b.WeeklyPoolContribution,
CalculatedAt = b.CalculatedAt?.ToDateTime(), CalculatedAt = b.CalculatedAt?.ToDateTime(),
IsExpired = b.IsExpired IsExpired = b.IsExpired,
// New fields for carryover info
LeftLegCarryover = b.LeftLegCarryover,
RightLegCarryover = b.RightLegCarryover,
LeftLegNewMembers = b.LeftLegNewMembers,
RightLegNewMembers = b.RightLegNewMembers
}).ToList(); }).ToList();
// Calculate summary // Calculate summary
@@ -46,9 +46,14 @@ public class WeeklyBalanceItemDto
public long Id { get; set; } public long Id { get; set; }
/// <summary> /// <summary>
/// شماره هفته (فرمت: "2025-W48") /// شناسه تعریف هفته
/// </summary> /// </summary>
public string WeekNumber { get; set; } = string.Empty; public long WeekDefinitionId { get; set; }
/// <summary>
/// برچسب هفته (مثال: "هفته اول")
/// </summary>
public string WeekLabel { get; set; } = string.Empty;
/// <summary> /// <summary>
/// تعادل پای چپ /// تعادل پای چپ
@@ -84,4 +89,24 @@ public class WeeklyBalanceItemDto
/// تاریخ شمسی /// تاریخ شمسی
/// </summary> /// </summary>
public string DatePersian { get; set; } = string.Empty; public string DatePersian { get; set; } = string.Empty;
/// <summary>
/// انتقالی پای چپ از هفته قبل
/// </summary>
public int LeftLegCarryover { get; set; }
/// <summary>
/// انتقالی پای راست از هفته قبل
/// </summary>
public int RightLegCarryover { get; set; }
/// <summary>
/// اعضای جدید پای چپ این هفته
/// </summary>
public int LeftLegNewMembers { get; set; }
/// <summary>
/// اعضای جدید پای راست این هفته
/// </summary>
public int RightLegNewMembers { get; set; }
} }
@@ -0,0 +1,25 @@
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetWeekDefinitions;
/// <summary>
/// دریافت لیست هفته‌ها برای dropdown
/// </summary>
public record GetWeekDefinitionsQuery : IRequest<GetWeekDefinitionsResponseDto>
{
//جستجوی متنی روی DisplayName
public string? SearchText { get; init; }
//شماره صفحه
public int PageNumber { get; init; } = 1;
//تعداد در صفحه
public int PageSize { get; init; } = 100;
//سال میلادی
public int? GregorianYear { get; init; }
//سال شمسی
public int? PersianYear { get; init; }
//فقط هفته‌های فعال
public bool? IsActive { get; init; }
}
@@ -0,0 +1,78 @@
using CMSMicroservice.Protobuf.Protos.Commission;
using FrontOffice.BFF.Application.Common.Models;
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetWeekDefinitions;
public class GetWeekDefinitionsQueryHandler : IRequestHandler<GetWeekDefinitionsQuery, GetWeekDefinitionsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetWeekDefinitionsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetWeekDefinitionsResponseDto> Handle(GetWeekDefinitionsQuery request, CancellationToken cancellationToken)
{
// ساخت درخواست به CMS
var cmsRequest = new GetWeekDefinitionsRequest
{
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = request.PageNumber,
PageSize = request.PageSize
}
};
// اعمال فیلتر
var filter = new GetWeekDefinitionsFilter();
if (!string.IsNullOrWhiteSpace(request.SearchText))
filter.SearchText = request.SearchText;
if (request.GregorianYear.HasValue)
filter.GregorianYear = request.GregorianYear.Value;
if (request.PersianYear.HasValue)
filter.PersianYear = request.PersianYear.Value;
if (request.IsActive.HasValue)
filter.IsActive = request.IsActive.Value;
cmsRequest.Filter = filter;
// فراخوانی CMS
var response = await _context.Commission.GetWeekDefinitionsAsync(cmsRequest, cancellationToken: cancellationToken);
// تبدیل به DTO
var weeks = response.Data.Select(w => new WeekDefinitionDto
{
Id = w.Id,
WeekOrder = w.WeekOrder,
DisplayName = w.DisplayName,
GregorianWeekNumber = w.GregorianWeekNumber,
PersianWeekNumber = w.PersianWeekNumber,
StartDate = w.StartDate?.ToDateTime() ?? DateTime.MinValue,
EndDate = w.EndDate?.ToDateTime() ?? DateTime.MinValue,
GregorianYear = w.GregorianYear,
PersianYear = w.PersianYear,
IsActive = w.IsActive,
IsCurrentWeek = w.IsCurrentWeek,
StartDatePersian = FormatPersianDate(w.StartDate?.ToDateTime()),
EndDatePersian = FormatPersianDate(w.EndDate?.ToDateTime())
}).ToList();
return new GetWeekDefinitionsResponseDto
{
Data = weeks,
TotalCount = response.TotalCount
};
}
private static string FormatPersianDate(DateTime? date)
{
if (!date.HasValue) return string.Empty;
// TODO: استفاده از PersianCalendar
return date.Value.ToString("yyyy/MM/dd");
}
}
@@ -0,0 +1,58 @@
namespace FrontOffice.BFF.Application.CommissionCQ.Queries.GetWeekDefinitions;
/// <summary>
/// پاسخ لیست هفته‌ها
/// </summary>
public class GetWeekDefinitionsResponseDto
{
//لیست هفته‌ها
public List<WeekDefinitionDto> Data { get; set; } = new();
//تعداد کل
public int TotalCount { get; set; }
}
/// <summary>
/// آیتم هفته برای dropdown
/// </summary>
public class WeekDefinitionDto
{
//شناسه تعریف هفته
public long Id { get; set; }
//شماره ترتیب هفته (1, 2, 3, ...)
public int WeekOrder { get; set; }
//نام نمایشی هفته (هفته یکم، هفته دوم، ...)
public string DisplayName { get; set; } = string.Empty;
//شماره هفته میلادی (2025-W46)
public string GregorianWeekNumber { get; set; } = string.Empty;
//شماره هفته شمسی (1404-W35)
public string PersianWeekNumber { get; set; } = string.Empty;
//تاریخ شروع هفته
public DateTime StartDate { get; set; }
//تاریخ پایان هفته
public DateTime EndDate { get; set; }
//سال میلادی
public int GregorianYear { get; set; }
//سال شمسی
public int PersianYear { get; set; }
//فعال بودن
public bool IsActive { get; set; }
//آیا هفته جاری است؟
public bool IsCurrentWeek { get; set; }
//تاریخ شروع به شمسی
public string StartDatePersian { get; set; } = string.Empty;
//تاریخ پایان به شمسی
public string EndDatePersian { get; set; } = string.Empty;
}
@@ -10,6 +10,7 @@ using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserCarts; using CMSMicroservice.Protobuf.Protos.UserCarts;
using CMSMicroservice.Protobuf.Protos.Commission; using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.Configuration; using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.UserContract; using CMSMicroservice.Protobuf.Protos.UserContract;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.Protobuf.Protos.UserWallet;
@@ -20,6 +21,7 @@ using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory; using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart; using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.Protobuf.Protos.DiscountOrder; using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using CMSMicroservice.Protobuf.Protos.City;
using PYMSMicroservice.Protobuf.Protos.Transaction; using PYMSMicroservice.Protobuf.Protos.Transaction;
namespace FrontOffice.BFF.Application.Common.Interfaces; namespace FrontOffice.BFF.Application.Common.Interfaces;
@@ -59,6 +61,12 @@ public interface IApplicationContractContext
DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories { get; } DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories { get; }
DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountCart { get; } DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountCart { get; }
DiscountOrderContract.DiscountOrderContractClient DiscountOrders { get; } DiscountOrderContract.DiscountOrderContractClient DiscountOrders { get; }
// Geography System (GMS)
CityContract.CityContractClient Cities { get; }
// App Version System
AppVersionContract.AppVersionContractClient AppVersion { get; }
#endregion #endregion
#region PYMS #region PYMS
@@ -0,0 +1,54 @@
using CMSMicroservice.Protobuf.Protos.City;
using FrontOffice.BFF.Application.CityCQ.Queries.GetAllCitiesByFilter;
using Mapster;
using CmsProtos = CMSMicroservice.Protobuf.Protos;
namespace FrontOffice.BFF.Application.Common.Mappings;
public class CityProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// Request: BFF → CMS
config.NewConfig<GetAllCitiesByFilterQuery, GetAllCitiesByFilterRequest>()
.Map(dest => dest.PaginationState, src => src.PaginationState)
.Map(dest => dest.SortBy, src => src.SortBy)
.Map(dest => dest.Filter, src => src.Filter);
// PaginationState is in CMSMicroservice.Protobuf.Protos namespace (from public_messages.proto)
config.NewConfig<PaginationStateDto, CmsProtos.PaginationState>()
.Map(dest => dest.PageNumber, src => src.PageNumber)
.Map(dest => dest.PageSize, src => src.PageSize);
config.NewConfig<GetAllCitiesByFilterFilterDto, GetAllCitiesByFilterFilter>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.Name, src => src.Name)
.Map(dest => dest.Native, src => src.Native)
.Map(dest => dest.StateId, src => src.StateId);
// Response: CMS → BFF
config.NewConfig<GetAllCitiesByFilterResponse, GetAllCitiesByFilterResponseDto>()
.Map(dest => dest.MetaData, src => src.MetaData)
.Map(dest => dest.Models, src => src.Models);
// MetaData is in CMSMicroservice.Protobuf.Protos namespace (from public_messages.proto)
config.NewConfig<CmsProtos.MetaData, MetaDataDto>()
.Map(dest => dest.CurrentPage, src => src.CurrentPage)
.Map(dest => dest.TotalPage, src => src.TotalPage)
.Map(dest => dest.PageSize, src => src.PageSize)
.Map(dest => dest.TotalCount, src => src.TotalCount)
.Map(dest => dest.HasPrevious, src => src.HasPrevious)
.Map(dest => dest.HasNext, src => src.HasNext);
config.NewConfig<GetAllCitiesByFilterResponseModel, CityDto>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.ExternalId, src => src.ExternalId)
.Map(dest => dest.Name, src => src.Name)
.Map(dest => dest.Native, src => src.Native)
.Map(dest => dest.Latitude, src => src.Latitude)
.Map(dest => dest.Longitude, src => src.Longitude)
.Map(dest => dest.StateId, src => src.StateId)
.Map(dest => dest.StateName, src => src.StateName)
.Map(dest => dest.StateNative, src => src.StateNative);
}
}
@@ -0,0 +1,8 @@
using FrontOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetAppVersion;
/// <summary>
/// دریافت نسخه اپلیکیشن برای بررسی نیاز به پاک کردن کش
/// </summary>
public record GetAppVersionQuery(string AppName, string? CurrentClientVersion) : IRequest<GetAppVersionResponseDto>;
@@ -0,0 +1,44 @@
using CMSMicroservice.Protobuf.Protos.AppVersion;
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetAppVersion;
/// <summary>
/// هندلر دریافت نسخه اپلیکیشن از CMS
/// </summary>
public class GetAppVersionQueryHandler : IRequestHandler<GetAppVersionQuery, GetAppVersionResponseDto>
{
private readonly IApplicationContractContext _context;
public GetAppVersionQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAppVersionResponseDto> Handle(GetAppVersionQuery request, CancellationToken cancellationToken)
{
var cmsRequest = new GetAppVersionRequest
{
AppName = request.AppName
};
if (!string.IsNullOrEmpty(request.CurrentClientVersion))
{
cmsRequest.CurrentClientVersion = request.CurrentClientVersion;
}
var response = await _context.AppVersion.GetAppVersionAsync(cmsRequest, cancellationToken: cancellationToken);
return new GetAppVersionResponseDto
{
Found = response.Found,
AppName = response.AppName,
CurrentVersion = response.CurrentVersion,
MinRequiredVersion = response.MinRequiredVersion,
RequiresFullCacheClear = response.RequiresFullCacheClear,
RequiresUpdate = response.RequiresUpdate,
UpdateMessage = response.UpdateMessage,
ReleaseNotes = response.ReleaseNotes,
LastUpdated = response.LastUpdated?.ToDateTime()
};
}
}
@@ -0,0 +1,17 @@
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetAppVersion;
/// <summary>
/// DTO پاسخ نسخه اپلیکیشن
/// </summary>
public class GetAppVersionResponseDto
{
public bool Found { get; set; }
public string AppName { get; set; } = string.Empty;
public string CurrentVersion { get; set; } = string.Empty;
public string MinRequiredVersion { get; set; } = string.Empty;
public bool RequiresFullCacheClear { get; set; }
public bool RequiresUpdate { get; set; }
public string? UpdateMessage { get; set; }
public string? ReleaseNotes { get; set; }
public DateTime? LastUpdated { get; set; }
}
@@ -0,0 +1,6 @@
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubConfiguration;
/// <summary>
/// دریافت تنظیمات باشگاه مشتریان
/// </summary>
public record GetClubConfigurationQuery : IRequest<GetClubConfigurationResponseDto>;
@@ -0,0 +1,43 @@
using CMSMicroservice.Protobuf.Protos.Configuration;
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubConfiguration;
/// <summary>
/// هندلر دریافت تنظیمات باشگاه مشتریان از CMS
/// </summary>
public class GetClubConfigurationQueryHandler : IRequestHandler<GetClubConfigurationQuery, GetClubConfigurationResponseDto>
{
private readonly IApplicationContractContext _context;
public GetClubConfigurationQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetClubConfigurationResponseDto> Handle(GetClubConfigurationQuery request, CancellationToken cancellationToken)
{
// دریافت مبلغ فعال‌سازی از CMS
var activationFeeRequest = new GetConfigurationByKeyRequest
{
Scope = 2, // ConfigurationScope.Club
Key = "Club.ActivationFee"
};
var giftValueRequest = new GetConfigurationByKeyRequest
{
Scope = 2, // ConfigurationScope.Club
Key = "Club.MembershipGiftValue"
};
var activationFeeResponse = await _context.Configuration.GetConfigurationByKeyAsync(activationFeeRequest, cancellationToken: cancellationToken);
var giftValueResponse = await _context.Configuration.GetConfigurationByKeyAsync(giftValueRequest, cancellationToken: cancellationToken);
var response = new GetClubConfigurationResponseDto
{
ActivationFee = long.TryParse(activationFeeResponse?.Value, out var fee) ? fee : 25000000,
MembershipGiftValue = long.TryParse(giftValueResponse?.Value, out var gift) ? gift : 25200000
};
return response;
}
}
@@ -0,0 +1,17 @@
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubConfiguration;
/// <summary>
/// پاسخ تنظیمات باشگاه مشتریان
/// </summary>
public class GetClubConfigurationResponseDto
{
/// <summary>
/// هزینه فعال‌سازی عضویت باشگاه (ریال)
/// </summary>
public long ActivationFee { get; set; }
/// <summary>
/// مبلغ هدیه حق عضویت باشگاه (ریال)
/// </summary>
public long MembershipGiftValue { get; set; }
}
@@ -0,0 +1,6 @@
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubFeatures;
/// <summary>
/// دریافت لیست فیچرهای باشگاه مشتریان برای کاربر جاری
/// </summary>
public record GetClubFeaturesQuery : IRequest<GetClubFeaturesResponseDto>;
@@ -0,0 +1,53 @@
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using FrontOffice.BFF.Application.Common.Interfaces;
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubFeatures;
/// <summary>
/// هندلر دریافت فیچرهای باشگاه مشتریان برای کاربر جاری
/// </summary>
public class GetClubFeaturesQueryHandler : IRequestHandler<GetClubFeaturesQuery, GetClubFeaturesResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUser;
public GetClubFeaturesQueryHandler(
IApplicationContractContext context,
ICurrentUserService currentUser)
{
_context = context;
_currentUser = currentUser;
}
public async Task<GetClubFeaturesResponseDto> Handle(GetClubFeaturesQuery request, CancellationToken cancellationToken)
{
// دریافت فیچرهای باشگاه برای کاربر جاری
var getUserFeaturesRequest = new GetUserClubFeaturesRequest
{
UserId = _currentUser.UserId ?? 0
};
var response = await _context.ClubMemberships.GetUserClubFeaturesAsync(
getUserFeaturesRequest,
cancellationToken: cancellationToken);
var result = new GetClubFeaturesResponseDto
{
Features = response.Features.Select(f => new ClubFeatureItemDto
{
Id = f.ClubFeatureId,
Title = f.FeatureTitle ?? string.Empty,
Description = f.FeatureDescription,
IsEnabled = f.IsActive,
DisplayOrder = f.SortOrder,
GrantedAt = f.GrantedAt?.ToDateTime() ?? DateTime.MinValue,
CreatedAt = f.CreatedAt?.ToDateTime() ?? DateTime.MinValue,
Notes = f.Notes
})
.OrderBy(f => f.DisplayOrder)
.ToList()
};
return result;
}
}
@@ -0,0 +1,18 @@
namespace FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubFeatures;
public class GetClubFeaturesResponseDto
{
public List<ClubFeatureItemDto> Features { get; set; } = new();
}
public class ClubFeatureItemDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsEnabled { get; set; }
public int DisplayOrder { get; set; }
public DateTime GrantedAt { get; set; }
public DateTime CreatedAt { get; set; }
public string? Notes { get; set; }
}
@@ -15,8 +15,8 @@
<ProjectReference Include="..\FrontOffice.BFF.Domain\FrontOffice.BFF.Domain.csproj" /> <ProjectReference Include="..\FrontOffice.BFF.Domain\FrontOffice.BFF.Domain.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.UserOrder.Protobuf\FrontOffice.BFF.UserOrder.Protobuf.csproj" /> <ProjectReference Include="..\Protobufs\FrontOffice.BFF.UserOrder.Protobuf\FrontOffice.BFF.UserOrder.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.Category.Protobuf\FrontOffice.BFF.Category.Protobuf.csproj" /> <ProjectReference Include="..\Protobufs\FrontOffice.BFF.Category.Protobuf\FrontOffice.BFF.Category.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.Configuration.Protobuf\FrontOffice.BFF.Configuration.Protobuf.csproj" />
<!-- CMS Protobuf for Commission, ClubMembership, NetworkMembership, Configuration --> <!-- CMS Protobuf for Commission, ClubMembership, NetworkMembership, Configuration -->
<ProjectReference Include="..\..\..\CMS\src\CMSMicroservice.Protobuf\CMSMicroservice.Protobuf.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -19,32 +19,50 @@ public class GetMyNetworkStatisticsQueryHandler : IRequestHandler<GetMyNetworkSt
{ {
var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated"); var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
// Note: GetNetworkStatisticsRequest is empty (returns overall stats) // Get user's own network info which contains personal stats AND subtree stats
// For user-specific stats, we need to use GetUserNetwork instead
var cmsRequest = new GetNetworkStatisticsRequest();
var response = await _context.NetworkMemberships.GetNetworkStatisticsAsync(cmsRequest, cancellationToken: cancellationToken);
// Also get user's own network info for personal stats
var userNetworkRequest = new GetUserNetworkRequest { UserId = userId }; var userNetworkRequest = new GetUserNetworkRequest { UserId = userId };
var userNetwork = await _context.NetworkMemberships.GetUserNetworkAsync(userNetworkRequest, cancellationToken: cancellationToken); var userNetwork = await _context.NetworkMemberships.GetUserNetworkAsync(userNetworkRequest, cancellationToken: cancellationToken);
var weakerLeg = response.LeftLegCount < response.RightLegCount ? "Left" : "Right"; // Calculate percentages from user's subtree stats
var totalSubtreeMembers = userNetwork.TotalLeftLegMembers + userNetwork.TotalRightLegMembers;
var leftPercentage = totalSubtreeMembers > 0 ? (double)userNetwork.TotalLeftLegMembers / totalSubtreeMembers * 100 : 0;
var rightPercentage = totalSubtreeMembers > 0 ? (double)userNetwork.TotalRightLegMembers / totalSubtreeMembers * 100 : 0;
// Find last member from TopUsers if available var weakerLeg = userNetwork.TotalLeftLegMembers < userNetwork.TotalRightLegMembers ? "Left" : "Right";
var lastMember = response.TopUsers.LastOrDefault();
// Get tree to find last member and calculate depth
var treeRequest = new GetNetworkTreeRequest
{
UserId = userId,
MaxDepth = 20 // Get deep enough to find last member
};
var treeResponse = await _context.NetworkMemberships.GetNetworkTreeAsync(treeRequest, cancellationToken: cancellationToken);
// Find the last joined member in user's subtree
var lastMember = treeResponse.Nodes
.Where(n => n.UserId != userId && n.JoinedAt != null)
.OrderByDescending(n => n.JoinedAt)
.FirstOrDefault();
// Calculate max depth from tree nodes
var maxDepth = treeResponse.Nodes.Count > 0
? treeResponse.Nodes.Max(n => n.NetworkLevel) - userNetwork.NetworkLevel
: 0;
// Calculate active members in user's subtree
var activeMembers = treeResponse.Nodes.Count(n => n.IsActive);
return new GetMyNetworkStatisticsResponseDto return new GetMyNetworkStatisticsResponseDto
{ {
// Overall network stats // User's subtree stats from GetUserNetwork
TotalMembers = response.TotalMembers, TotalMembers = userNetwork.TotalNetworkSize,
ActiveMembers = response.ActiveMembers, ActiveMembers = activeMembers,
LeftLegCount = response.LeftLegCount, LeftLegCount = userNetwork.TotalLeftLegMembers,
RightLegCount = response.RightLegCount, RightLegCount = userNetwork.TotalRightLegMembers,
LeftPercentage = response.LeftPercentage, LeftPercentage = Math.Round(leftPercentage, 2),
RightPercentage = response.RightPercentage, RightPercentage = Math.Round(rightPercentage, 2),
AverageDepth = response.AverageDepth, AverageDepth = 0, // Not available from current API
MaxDepth = response.MaxDepth, MaxDepth = Math.Max(maxDepth, userNetwork.MaxNetworkDepth),
WeakerLeg = weakerLeg, WeakerLeg = weakerLeg,
// User's personal info // User's personal info
@@ -52,13 +70,13 @@ public class GetMyNetworkStatisticsQueryHandler : IRequestHandler<GetMyNetworkSt
MyNetworkLeg = userNetwork.NetworkLeg == 0 ? "Left" : "Right", MyNetworkLeg = userNetwork.NetworkLeg == 0 ? "Left" : "Right",
MyReferralCode = userNetwork.ReferralCode, MyReferralCode = userNetwork.ReferralCode,
// Last member info // Last member info from tree
LastMember = lastMember != null ? new LastMemberDto LastMember = lastMember != null ? new LastMemberDto
{ {
UserId = lastMember.UserId, UserId = lastMember.UserId,
FullName = lastMember.UserName, FullName = lastMember.UserName ?? string.Empty,
Position = lastMember.LeftCount > lastMember.RightCount ? "Left" : "Right", Position = lastMember.NetworkLeg == 0 ? "Left" : "Right",
TotalChildren = lastMember.TotalChildren TotalChildren = 0 // Would need another call to get this
} : null } : null
}; };
} }
@@ -21,7 +21,7 @@ public class GetMyNetworkTreeQueryHandler : IRequestHandler<GetMyNetworkTreeQuer
var cmsRequest = new GetNetworkTreeRequest var cmsRequest = new GetNetworkTreeRequest
{ {
RootUserId = userId, UserId = userId,
MaxDepth = Math.Clamp(request.MaxDepth, 1, 10) // محدود کردن بین 1-10 MaxDepth = Math.Clamp(request.MaxDepth, 1, 10) // محدود کردن بین 1-10
}; };
@@ -81,6 +81,11 @@ public class GetMyNetworkTreeQueryHandler : IRequestHandler<GetMyNetworkTreeQuer
Avatar = null, // Proto doesn't have avatar Avatar = null, // Proto doesn't have avatar
Position = position, Position = position,
Level = level, Level = level,
IsActive = cmsNode.IsActive,
JoinedAt = cmsNode.JoinedAt?.ToDateTime(),
IsClubActive = cmsNode.IsClubActive,
ActivationWeekDefinitionId = cmsNode.ActivationWeekDefinitionId,
ReferralCode = cmsNode.ReferralCode,
LeftChild = leftChild != null ? BuildNodeRecursive(leftChild, allNodes, nodeDict, level + 1) : null, LeftChild = leftChild != null ? BuildNodeRecursive(leftChild, allNodes, nodeDict, level + 1) : null,
RightChild = rightChild != null ? BuildNodeRecursive(rightChild, allNodes, nodeDict, level + 1) : null RightChild = rightChild != null ? BuildNodeRecursive(rightChild, allNodes, nodeDict, level + 1) : null
}; };
@@ -64,4 +64,29 @@ public class NetworkNodeDto
/// آیا فرزند دارد؟ /// آیا فرزند دارد؟
/// </summary> /// </summary>
public bool HasChildren => LeftChild != null || RightChild != null; public bool HasChildren => LeftChild != null || RightChild != null;
/// <summary>
/// آیا فعال است؟
/// </summary>
public bool IsActive { get; set; }
/// <summary>
/// تاریخ عضویت در شبکه
/// </summary>
public DateTime? JoinedAt { get; set; }
/// <summary>
/// آیا در باشگاه فعال است؟
/// </summary>
public bool IsClubActive { get; set; }
/// <summary>
/// شماره هفته فعال‌سازی
/// </summary>
public long? ActivationWeekDefinitionId { get; set; }
/// <summary>
/// کد معرف کاربر
/// </summary>
public string? ReferralCode { get; set; }
} }
@@ -0,0 +1,18 @@
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
/// <summary>
/// Query برای دریافت درخت شبکه یک زیرمجموعه
/// با چک امنیتی که فقط زیرمجموعه‌های خود کاربر قابل مشاهده باشند
/// </summary>
public record GetSubordinateTreeQuery : IRequest<GetSubordinateTreeResponseDto>
{
/// <summary>
/// شناسه کاربر زیرمجموعه که می‌خواهیم درختش را ببینیم
/// </summary>
public long TargetUserId { get; init; }
/// <summary>
/// حداکثر عمق درخت (پیش‌فرض: 3)
/// </summary>
public int MaxDepth { get; init; } = 3;
}
@@ -0,0 +1,107 @@
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
/// <summary>
/// Handler برای دریافت درخت زیرمجموعه
/// امنیت: کاربر باید احراز هویت شده باشد (توکن معتبر)
/// CMS خودش چک می‌کند که آیا targetUserId در زیرمجموعه‌های کاربر توکن هست یا نه
/// </summary>
public class GetSubordinateTreeQueryHandler : IRequestHandler<GetSubordinateTreeQuery, GetSubordinateTreeResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
public GetSubordinateTreeQueryHandler(
IApplicationContractContext context,
ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<GetSubordinateTreeResponseDto> Handle(GetSubordinateTreeQuery request, CancellationToken cancellationToken)
{
var currentUserId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated");
var targetUserId = request.TargetUserId;
// اگر کاربر درخت خودش را می‌خواهد
if (targetUserId == currentUserId || targetUserId == 0)
{
targetUserId = currentUserId;
}
var cmsRequest = new GetNetworkTreeRequest
{
UserId = targetUserId,
MaxDepth = Math.Clamp(request.MaxDepth, 1, 15)
};
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken: cancellationToken);
var rootNode = BuildTreeFromFlatList(response.Nodes.ToList(), targetUserId);
return new GetSubordinateTreeResponseDto
{
RootNode = rootNode,
TotalMembers = response.Nodes.Count,
CurrentDepth = CalculateDepth(rootNode),
CanGoBack = targetUserId != currentUserId,
ParentUserId = targetUserId != currentUserId ? currentUserId : null
};
}
/// <summary>
/// Build hierarchical tree from flat list
/// </summary>
private NetworkNodeDto? BuildTreeFromFlatList(List<NetworkTreeNodeModel> flatNodes, long rootUserId)
{
if (flatNodes == null || flatNodes.Count == 0)
return null;
var nodeDict = flatNodes.ToDictionary(n => n.UserId);
var rootCmsNode = flatNodes.FirstOrDefault(n => n.UserId == rootUserId);
if (rootCmsNode == null)
return null;
return BuildNodeRecursive(rootCmsNode, flatNodes, nodeDict, 0);
}
private NetworkNodeDto BuildNodeRecursive(
NetworkTreeNodeModel cmsNode,
List<NetworkTreeNodeModel> allNodes,
Dictionary<long, NetworkTreeNodeModel> nodeDict,
int level)
{
var leftChild = allNodes.FirstOrDefault(n =>
n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 0);
var rightChild = allNodes.FirstOrDefault(n =>
n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 1);
var position = level == 0 ? "Root" : (cmsNode.NetworkLeg == 0 ? "Left" : "Right");
return new NetworkNodeDto
{
UserId = cmsNode.UserId,
FullName = cmsNode.UserName ?? string.Empty,
Mobile = string.Empty,
Avatar = null,
Position = position,
Level = level,
IsActive = cmsNode.IsActive,
JoinedAt = cmsNode.JoinedAt?.ToDateTime(),
IsClubActive = cmsNode.IsClubActive,
ReferralCode = cmsNode.ReferralCode,
ActivationWeekDefinitionId = cmsNode.ActivationWeekDefinitionId,
LeftChild = leftChild != null ? BuildNodeRecursive(leftChild, allNodes, nodeDict, level + 1) : null,
RightChild = rightChild != null ? BuildNodeRecursive(rightChild, allNodes, nodeDict, level + 1) : null
};
}
private int CalculateDepth(NetworkNodeDto? node)
{
if (node == null) return 0;
return 1 + Math.Max(CalculateDepth(node.LeftChild), CalculateDepth(node.RightChild));
}
}
@@ -0,0 +1,35 @@
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
/// <summary>
/// Response برای درخت زیرمجموعه - ساختار مشابه GetMyNetworkTreeResponseDto
/// </summary>
public class GetSubordinateTreeResponseDto
{
/// <summary>
/// نود ریشه (کاربر زیرمجموعه انتخاب شده)
/// </summary>
public NetworkNodeDto? RootNode { get; set; }
/// <summary>
/// تعداد کل اعضا در این زیردرخت
/// </summary>
public int TotalMembers { get; set; }
/// <summary>
/// عمق فعلی درخت
/// </summary>
public int CurrentDepth { get; set; }
/// <summary>
/// آیا کاربر می‌تواند به عقب برگردد (root خودش نیست)
/// </summary>
public bool CanGoBack { get; set; }
/// <summary>
/// شناسه parent برای بازگشت به عقب
/// </summary>
public long? ParentUserId { get; set; }
}
@@ -0,0 +1,51 @@
namespace FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
/// <summary>
/// درخواست پرداخت پکیج پایه 56 میلیون تومان
/// UserId از CurrentUserService گرفته می‌شود
/// </summary>
public record InitiateBasePackagePaymentCommand : IRequest<InitiateBasePackagePaymentResponseDto>
{
/// <summary>
/// آدرس بازگشت از درگاه پرداخت
/// </summary>
public string CallbackUrl { get; init; } = string.Empty;
}
public class InitiateBasePackagePaymentResponseDto
{
/// <summary>
/// آیا عملیات موفق بود؟
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// شناسه سفارش CMS
/// </summary>
public long OrderId { get; set; }
/// <summary>
/// شناسه تراکنش CMS
/// </summary>
public long TransactionId { get; set; }
/// <summary>
/// مبلغ پرداختی (ریال)
/// </summary>
public long Amount { get; set; }
/// <summary>
/// آدرس درگاه پرداخت
/// </summary>
public string PaymentGatewayUrl { get; set; } = string.Empty;
/// <summary>
/// کد پیگیری زرین‌پال
/// </summary>
public string Authority { get; set; } = string.Empty;
}
@@ -0,0 +1,100 @@
using CmsPackage = CMSMicroservice.Protobuf.Protos.Package;
using PYMSMicroservice.Protobuf.Protos;
using PYMSMicroservice.Protobuf.Protos.Transaction;
namespace FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
/// <summary>
/// Handler برای درخواست پرداخت پکیج پایه 56 میلیون تومان
/// 1. ابتدا CMS فراخوانی می‌شود برای ثبت Transaction و Order
/// 2. سپس PYMS فراخوانی می‌شود برای دریافت URL درگاه پرداخت
/// </summary>
public class InitiateBasePackagePaymentCommandHandler : IRequestHandler<InitiateBasePackagePaymentCommand, InitiateBasePackagePaymentResponseDto>
{
private readonly IApplicationContractContext _context;
private readonly ICurrentUserService _currentUserService;
private const string ZarinpalMerchantId = "6b098fc8-f490-47a1-aac3-1de1a1b84404";
public InitiateBasePackagePaymentCommandHandler(IApplicationContractContext context, ICurrentUserService currentUserService)
{
_context = context;
_currentUserService = currentUserService;
}
public async Task<InitiateBasePackagePaymentResponseDto> Handle(InitiateBasePackagePaymentCommand request, CancellationToken cancellationToken)
{
// 1. ابتدا CMS را فراخوانی کن برای ثبت تراکنش و سفارش
var cmsRequest = new CmsPackage.InitiateBasePackagePaymentRequest
{
UserId = _currentUserService.UserId.Value
};
var cmsResponse = await _context.Package.InitiateBasePackagePaymentAsync(
cmsRequest,
cancellationToken: cancellationToken);
// اگر CMS خطا داد
if (!cmsResponse.Success)
{
return new InitiateBasePackagePaymentResponseDto
{
Success = false,
Message = cmsResponse.Message
};
}
// 2. حالا PYMS را برای دریافت URL درگاه فراخوانی کن
// مبلغ به ریال (56 میلیون تومان = 560,000,000 ریال)
var paymentRequest = new PaymentRequestRequest
{
MerchantId = ZarinpalMerchantId,
Amount = cmsResponse.Amount * 10, // تبدیل تومان به ریال
CallbackUrl = $"{request.CallbackUrl}?orderId={cmsResponse.OrderId}&transactionId={cmsResponse.TransactionId}",
Description = "پرداخت پکیج پایه",
Currency = CurrencyEnum.Irr,
Type = TransactionTypeEnum.Sandbox,
OrderId = cmsResponse.OrderId.ToString()
};
var paymentResponse = await _context.ZarinTransactions.PaymentRequestAsync(
paymentRequest,
cancellationToken: cancellationToken);
// اگر درگاه URL برنگردانده
if (string.IsNullOrEmpty(paymentResponse.PaymentGWUrl))
{
return new InitiateBasePackagePaymentResponseDto
{
Success = false,
Message = "خطا در دریافت آدرس درگاه پرداخت",
OrderId = cmsResponse.OrderId,
TransactionId = cmsResponse.TransactionId
};
}
// استخراج Authority از URL
// URL معمولا به شکل: https://www.zarinpal.com/pg/StartPay/{Authority}
var authority = ExtractAuthorityFromUrl(paymentResponse.PaymentGWUrl);
return new InitiateBasePackagePaymentResponseDto
{
Success = true,
Message = "لطفا برای تکمیل پرداخت به درگاه بانکی مراجعه کنید",
OrderId = cmsResponse.OrderId,
TransactionId = cmsResponse.TransactionId,
Amount = cmsResponse.Amount,
PaymentGatewayUrl = paymentResponse.PaymentGWUrl,
Authority = authority
};
}
private static string ExtractAuthorityFromUrl(string url)
{
// URL: https://www.zarinpal.com/pg/StartPay/{Authority}
if (string.IsNullOrEmpty(url))
return string.Empty;
var parts = url.TrimEnd('/').Split('/');
return parts.Length > 0 ? parts[^1] : string.Empty;
}
}
@@ -0,0 +1,15 @@
namespace FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
public class InitiateBasePackagePaymentCommandValidator : AbstractValidator<InitiateBasePackagePaymentCommand>
{
public InitiateBasePackagePaymentCommandValidator()
{
// RuleFor(x => x.UserId)
// .GreaterThan(0)
// .WithMessage("شناسه کاربر الزامی است");
RuleFor(x => x.CallbackUrl)
.NotEmpty()
.WithMessage("آدرس بازگشت الزامی است");
}
}
@@ -0,0 +1,65 @@
namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
/// <summary>
/// تأیید پرداخت پکیج پایه (Callback از درگاه)
/// </summary>
public record VerifyBasePackagePaymentCommand : IRequest<VerifyBasePackagePaymentResponseDto>
{
/// <summary>
/// شناسه سفارش CMS
/// </summary>
public long OrderId { get; init; }
/// <summary>
/// شناسه تراکنش CMS
/// </summary>
public long TransactionId { get; init; }
/// <summary>
/// کد پیگیری از زرین‌پال
/// </summary>
public string Authority { get; init; } = string.Empty;
/// <summary>
/// وضعیت از زرین‌پال (OK یا NOK)
/// </summary>
public string Status { get; init; } = string.Empty;
}
public class VerifyBasePackagePaymentResponseDto
{
/// <summary>
/// آیا پرداخت موفق بود؟
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; set; }
/// <summary>
/// شناسه تراکنش
/// </summary>
public long TransactionId { get; set; }
/// <summary>
/// کد پیگیری بانکی
/// </summary>
public string? RefId { get; set; }
/// <summary>
/// موجودی کیف پول بعد از شارژ
/// </summary>
public long WalletBalance { get; set; }
/// <summary>
/// موجودی تخفیف بعد از شارژ
/// </summary>
public long DiscountBalance { get; set; }
}
@@ -0,0 +1,105 @@
using CmsPackage = CMSMicroservice.Protobuf.Protos.Package;
using PYMSMicroservice.Protobuf.Protos.Transaction;
namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
/// <summary>
/// Handler برای تأیید پرداخت پکیج پایه
/// 1. ابتدا PYMS فراخوانی می‌شود برای Verify پرداخت
/// 2. سپس CMS فراخوانی می‌شود برای تکمیل یا رد تراکنش
/// </summary>
public class VerifyBasePackagePaymentCommandHandler : IRequestHandler<VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponseDto>
{
private readonly IApplicationContractContext _context;
public VerifyBasePackagePaymentCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<VerifyBasePackagePaymentResponseDto> Handle(VerifyBasePackagePaymentCommand request, CancellationToken cancellationToken)
{
// اگر وضعیت از زرین‌پال NOK باشد، کاربر پرداخت را لغو کرده
if (request.Status != "OK")
{
// به CMS اطلاع بده که پرداخت ناموفق بود
await NotifyCmsPaymentFailed(request.OrderId, request.TransactionId, "پرداخت توسط کاربر لغو شد", cancellationToken);
return new VerifyBasePackagePaymentResponseDto
{
Success = false,
Message = "پرداخت لغو شد",
OrderId = request.OrderId,
TransactionId = request.TransactionId
};
}
// 1. تأیید پرداخت از زرین‌پال
var verifyRequest = new PaymentVerificationRequest
{
Authority = request.Authority,
Status = request.Status,
};
var verifyResponse = await _context.ZarinTransactions.PaymentVerificationAsync(
verifyRequest,
cancellationToken: cancellationToken);
// 2. بر اساس نتیجه Verify، CMS را به‌روزرسانی کن
if (!verifyResponse.PaymentStatus)
{
// پرداخت ناموفق بود
await NotifyCmsPaymentFailed(request.OrderId, request.TransactionId, verifyResponse.Message, cancellationToken);
return new VerifyBasePackagePaymentResponseDto
{
Success = false,
Message = verifyResponse.Message ?? "پرداخت ناموفق بود",
OrderId = request.OrderId,
TransactionId = request.TransactionId
};
}
// پرداخت موفق بود - CMS را برای شارژ کیف پول فراخوانی کن
var cmsRequest = new CmsPackage.VerifyBasePackagePaymentRequest
{
OrderId = request.OrderId,
TransactionId = request.TransactionId,
PaymentSuccess = true,
RefId = verifyResponse.RefId,
Message = verifyResponse.Message
};
var cmsResponse = await _context.Package.VerifyBasePackagePaymentAsync(
cmsRequest,
cancellationToken: cancellationToken);
return new VerifyBasePackagePaymentResponseDto
{
Success = cmsResponse.Success,
Message = cmsResponse.Message,
OrderId = cmsResponse.OrderId,
TransactionId = cmsResponse.TransactionId,
RefId = cmsResponse.ReferenceCode,
WalletBalance = cmsResponse.WalletBalance,
DiscountBalance = cmsResponse.DiscountBalance
};
}
/// <summary>
/// اطلاع دادن به CMS که پرداخت ناموفق بود
/// </summary>
private async Task NotifyCmsPaymentFailed(long orderId, long transactionId, string reason, CancellationToken cancellationToken)
{
var cmsRequest = new CmsPackage.VerifyBasePackagePaymentRequest
{
OrderId = orderId,
TransactionId = transactionId,
PaymentSuccess = false,
Message = reason
};
await _context.Package.VerifyBasePackagePaymentAsync(cmsRequest, cancellationToken: cancellationToken);
}
}
@@ -0,0 +1,23 @@
namespace FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
public class VerifyBasePackagePaymentCommandValidator : AbstractValidator<VerifyBasePackagePaymentCommand>
{
public VerifyBasePackagePaymentCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش الزامی است");
RuleFor(x => x.TransactionId)
.GreaterThan(0)
.WithMessage("شناسه تراکنش الزامی است");
RuleFor(x => x.Authority)
.NotEmpty()
.WithMessage("کد پیگیری الزامی است");
RuleFor(x => x.Status)
.NotEmpty()
.WithMessage("وضعیت پرداخت الزامی است");
}
}
@@ -0,0 +1,14 @@
using MediatR;
namespace FrontOffice.BFF.Application.UserCQ.Commands.RefreshToken;
/// <summary>
/// درخواست رفرش توکن از BFF
/// </summary>
public sealed record RefreshTokenCommand : IRequest<RefreshTokenResponseDto>
{
/// <summary>
/// توکن فعلی کاربر
/// </summary>
public string CurrentToken { get; init; } = null!;
}
@@ -0,0 +1,47 @@
using FrontOffice.BFF.Application.Common.Interfaces;
using Mapster;
using MediatR;
namespace FrontOffice.BFF.Application.UserCQ.Commands.RefreshToken;
/// <summary>
/// هندلر رفرش توکن - فراخوانی CMS برای دریافت توکن جدید
/// </summary>
public class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, RefreshTokenResponseDto>
{
private readonly IApplicationContractContext _context;
public RefreshTokenCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<RefreshTokenResponseDto> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
{
try
{
var cmsRequest = new CMSMicroservice.Protobuf.Protos.User.RefreshTokenRequest
{
CurrentToken = request.CurrentToken
};
var response = await _context.User.RefreshTokenAsync(cmsRequest, cancellationToken: cancellationToken);
return new RefreshTokenResponseDto
{
Token = response.Token,
Success = response.Success,
Message = response.Message
};
}
catch (Exception ex)
{
return new RefreshTokenResponseDto
{
Success = false,
Message = $"خطا در رفرش توکن: {ex.Message}",
Token = string.Empty
};
}
}
}
@@ -0,0 +1,22 @@
namespace FrontOffice.BFF.Application.UserCQ.Commands.RefreshToken;
/// <summary>
/// پاسخ رفرش توکن
/// </summary>
public class RefreshTokenResponseDto
{
/// <summary>
/// توکن جدید
/// </summary>
public string Token { get; set; } = null!;
/// <summary>
/// آیا عملیات موفق بود؟
/// </summary>
public bool Success { get; set; }
/// <summary>
/// پیام
/// </summary>
public string Message { get; set; } = null!;
}
@@ -25,8 +25,38 @@ public class GetUserOrderResponseDto
public string? UserAddressText { get; set; } public string? UserAddressText { get; set; }
// //
public List<GetUserOrderResponseFactorDetail>? FactorDetails { get; set; } public List<GetUserOrderResponseFactorDetail>? FactorDetails { get; set; }
// اطلاعات مالیات بر ارزش افزوده
public OrderVATInfoDto? VatInfo { get; set; }
}
}public class GetUserOrderResponseFactorDetail /// <summary>
/// اطلاعات مالیات بر ارزش افزوده
/// </summary>
public class OrderVATInfoDto
{
/// <summary>
/// نرخ مالیات (مثلاً 0.09 = 9%)
/// </summary>
public double VatRate { get; set; }
/// <summary>
/// مبلغ پایه (قبل از مالیات)
/// </summary>
public long BaseAmount { get; set; }
/// <summary>
/// مبلغ مالیات
/// </summary>
public long VatAmount { get; set; }
/// <summary>
/// مبلغ کل (پایه + مالیات)
/// </summary>
public long TotalAmount { get; set; }
/// <summary>
/// آیا پرداخت شده
/// </summary>
public bool IsPaid { get; set; }
}
public class GetUserOrderResponseFactorDetail
{ {
//شناسه //شناسه
public long ProductId { get; set; } public long ProductId { get; set; }
@@ -0,0 +1,6 @@
namespace FrontOffice.BFF.Application.UserOrderCQ.Queries.GetVATRate;
/// <summary>
/// دریافت نرخ مالیات بر ارزش افزوده
/// </summary>
public record GetVATRateQuery : IRequest<GetVATRateResponseDto>;
@@ -0,0 +1,68 @@
using CMSMicroservice.Protobuf.Protos.Configuration;
namespace FrontOffice.BFF.Application.UserOrderCQ.Queries.GetVATRate;
/// <summary>
/// هندلر دریافت نرخ مالیات بر ارزش افزوده
/// </summary>
public class GetVATRateQueryHandler : IRequestHandler<GetVATRateQuery, GetVATRateResponseDto>
{
private readonly IApplicationContractContext _context;
public GetVATRateQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetVATRateResponseDto> Handle(GetVATRateQuery request, CancellationToken cancellationToken)
{
// Default values
var response = new GetVATRateResponseDto
{
VatRate = 0.09, // 9% default
VatPercentage = 9,
IsEnabled = true
};
try
{
// دریافت تنظیمات VAT از CMS
// Scope: VAT = 4
var allConfigs = await _context.Configuration.GetAllConfigurationsAsync(
new GetAllConfigurationsRequest
{
Filter = new GetAllConfigurationsFilter
{
Scope = 4, // VAT scope
IsActive = true
}
},
cancellationToken: cancellationToken);
if (allConfigs?.Models != null)
{
foreach (var config in allConfigs.Models)
{
if (config.Key == "Shop.VAT" && !string.IsNullOrEmpty(config.Value))
{
if (double.TryParse(config.Value, out var rate))
{
response.VatRate = rate;
response.VatPercentage = (int)(rate * 100);
}
}
else if (config.Key == "IsEnabled" && !string.IsNullOrEmpty(config.Value))
{
response.IsEnabled = bool.TryParse(config.Value, out var enabled) && enabled;
}
}
}
}
catch
{
// در صورت خطا، مقادیر پیش‌فرض برگردانده می‌شود
}
return response;
}
}
@@ -0,0 +1,22 @@
namespace FrontOffice.BFF.Application.UserOrderCQ.Queries.GetVATRate;
/// <summary>
/// پاسخ نرخ مالیات بر ارزش افزوده
/// </summary>
public class GetVATRateResponseDto
{
/// <summary>
/// نرخ مالیات (مثلاً 0.09 = 9%)
/// </summary>
public double VatRate { get; set; }
/// <summary>
/// درصد مالیات (مثلاً 9)
/// </summary>
public int VatPercentage { get; set; }
/// <summary>
/// آیا مالیات فعال است
/// </summary>
public bool IsEnabled { get; set; }
}
@@ -30,7 +30,8 @@ public class GetUserWithdrawalsQueryHandler : IRequestHandler<GetUserWithdrawals
.Select(m => new UserWithdrawalModel .Select(m => new UserWithdrawalModel
{ {
Id = m.Id, Id = m.Id,
WeekNumber = m.WeekNumber, WeekDefinitionId = m.WeekDefinitionId,
WeekDisplayName = m.WeekDisplayName,
TotalAmount = m.TotalAmount, TotalAmount = m.TotalAmount,
Status = m.Status, Status = m.Status,
WithdrawalMethod = m.WithdrawalMethod, // int? type - no .Value needed WithdrawalMethod = m.WithdrawalMethod, // int? type - no .Value needed
@@ -8,7 +8,8 @@ public class GetUserWithdrawalsResponseDto
public class UserWithdrawalModel public class UserWithdrawalModel
{ {
public long Id { get; set; } public long Id { get; set; }
public string WeekNumber { get; set; } = string.Empty; public long WeekDefinitionId { get; set; }
public string WeekDisplayName { get; set; } = string.Empty;
public long TotalAmount { get; set; } public long TotalAmount { get; set; }
public int Status { get; set; } public int Status { get; set; }
public int? WithdrawalMethod { get; set; } public int? WithdrawalMethod { get; set; }
@@ -7,7 +7,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Afrino.PYMSMicroservice.Protobuf" Version="0.0.11" /> <PackageReference Include="Afrino.PYMSMicroservice.Protobuf" Version="0.0.11" />
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.139" /> <PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.162" />
<PackageReference Include="Google.Protobuf" Version="3.33.0" /> <PackageReference Include="Google.Protobuf" Version="3.33.0" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.54.0" /> <PackageReference Include="Grpc.Net.ClientFactory" Version="2.54.0" />
<PackageReference Include="Grpc.Tools" Version="2.76.0"> <PackageReference Include="Grpc.Tools" Version="2.76.0">
@@ -16,4 +16,9 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<!-- <ItemGroup>-->
<!-- &lt;!&ndash; Direct project reference to local CMS Protobuf for development &ndash;&gt;-->
<!-- <ProjectReference Include="../../../CMS/src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj" />-->
<!-- </ItemGroup>-->
</Project> </Project>
@@ -10,6 +10,7 @@ using CMSMicroservice.Protobuf.Protos.UserAddress;
using CMSMicroservice.Protobuf.Protos.UserCarts; using CMSMicroservice.Protobuf.Protos.UserCarts;
using CMSMicroservice.Protobuf.Protos.Commission; using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.Configuration; using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.UserContract; using CMSMicroservice.Protobuf.Protos.UserContract;
using CMSMicroservice.Protobuf.Protos.UserOrder; using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserWallet; using CMSMicroservice.Protobuf.Protos.UserWallet;
@@ -20,6 +21,7 @@ using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory; using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart; using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.Protobuf.Protos.DiscountOrder; using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using CMSMicroservice.Protobuf.Protos.City;
using FrontOffice.BFF.Application.Common.Interfaces; using FrontOffice.BFF.Application.Common.Interfaces;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using PYMSMicroservice.Protobuf.Protos.Transaction; using PYMSMicroservice.Protobuf.Protos.Transaction;
@@ -86,6 +88,12 @@ public class ApplicationContractContext : IApplicationContractContext
public DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories => GetService<DiscountCategoryContract.DiscountCategoryContractClient>(); public DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories => GetService<DiscountCategoryContract.DiscountCategoryContractClient>();
public DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountCart => GetService<DiscountShoppingCartContract.DiscountShoppingCartContractClient>(); public DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountCart => GetService<DiscountShoppingCartContract.DiscountShoppingCartContractClient>();
public DiscountOrderContract.DiscountOrderContractClient DiscountOrders => GetService<DiscountOrderContract.DiscountOrderContractClient>(); public DiscountOrderContract.DiscountOrderContractClient DiscountOrders => GetService<DiscountOrderContract.DiscountOrderContractClient>();
// Geography System (GMS)
public CityContract.CityContractClient Cities => GetService<CityContract.CityContractClient>();
// App Version System
public AppVersionContract.AppVersionContractClient AppVersion => GetService<AppVersionContract.AppVersionContractClient>();
#endregion #endregion
#region PYMS #region PYMS
@@ -0,0 +1,184 @@
using System.Threading;
using FrontOffice.BFF.WebApi.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace FrontOffice.BFF.WebApi.BackgroundServices;
/// <summary>
/// Background service that connects to CMS SignalR Hub and relays token notifications to Frontend clients.
/// This service maintains a persistent connection to CMS and forwards messages to the appropriate users.
/// </summary>
public class CmsSignalRClientService : BackgroundService
{
private readonly ILogger<CmsSignalRClientService> _logger;
private readonly IConfiguration _configuration;
private readonly IHubContext<TokenRelayHub> _hubContext;
private HubConnection? _cmsHubConnection;
public CmsSignalRClientService(
ILogger<CmsSignalRClientService> logger,
IConfiguration configuration,
IHubContext<TokenRelayHub> hubContext)
{
_logger = logger;
_configuration = configuration;
_hubContext = hubContext;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ConnectToCmsHubAsync(stoppingToken);
// Keep the connection alive
while (_cmsHubConnection?.State == HubConnectionState.Connected && !stoppingToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("CMS SignalR client service is stopping");
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in CMS SignalR connection. Retrying in 5 seconds...");
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}
private async Task ConnectToCmsHubAsync(CancellationToken stoppingToken)
{
var cmsBaseUrl = _configuration["GrpcChannelOptions:CMSMSAddress"]?.TrimEnd('/') ?? "http://localhost:5000";
var hubPath = _configuration["CmsSignalR:HubPath"] ?? "/hubs/token-notification";
var cmsSignalRUrl = $"{cmsBaseUrl}{hubPath}";
_logger.LogInformation("Connecting to CMS SignalR Hub at {Url}", cmsSignalRUrl);
_cmsHubConnection = new HubConnectionBuilder()
.WithUrl(cmsSignalRUrl)
.WithAutomaticReconnect(new[] { TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10) })
.Build();
// Register event handlers for CMS notifications
RegisterEventHandlers();
// Connect to CMS Hub
await _cmsHubConnection.StartAsync(stoppingToken);
_logger.LogInformation("Successfully connected to CMS SignalR Hub");
// Handle reconnection events
_cmsHubConnection.Reconnecting += error =>
{
_logger.LogWarning(error, "CMS SignalR connection lost. Attempting to reconnect...");
return Task.CompletedTask;
};
_cmsHubConnection.Reconnected += connectionId =>
{
_logger.LogInformation("CMS SignalR reconnected. ConnectionId: {ConnectionId}", connectionId);
return Task.CompletedTask;
};
_cmsHubConnection.Closed += async error =>
{
_logger.LogWarning(error, "CMS SignalR connection closed");
await Task.CompletedTask;
};
}
private void RegisterEventHandlers()
{
if (_cmsHubConnection == null) return;
// Handle TokenRevoked event from CMS
_cmsHubConnection.On<TokenRevokedNotification>("TokenRevoked", async notification =>
{
_logger.LogInformation("Received TokenRevoked for user {UserId}. Reason: {Reason}",
notification.UserId, notification.Reason);
// Relay to Frontend clients subscribed to this user
await _hubContext.Clients.Group($"user_{notification.UserId}")
.SendAsync("TokenRevoked", new
{
notification.UserId,
notification.Reason,
notification.Timestamp
});
});
// Handle ForceRefreshToken event from CMS
_cmsHubConnection.On<ForceRefreshNotification>("ForceRefreshToken", async notification =>
{
_logger.LogInformation("Received ForceRefreshToken for user {UserId}", notification.UserId);
// Relay to Frontend clients subscribed to this user
await _hubContext.Clients.Group($"user_{notification.UserId}")
.SendAsync("ForceRefreshToken", new
{
notification.UserId,
notification.Timestamp
});
});
// Handle BroadcastMessage event from CMS
_cmsHubConnection.On<BroadcastNotification>("BroadcastMessage", async notification =>
{
_logger.LogInformation("Received BroadcastMessage: {Message}", notification.Message);
// Relay to all Frontend clients
await _hubContext.Clients.All.SendAsync("BroadcastMessage", new
{
notification.Message,
notification.Timestamp
});
});
}
public override async Task StopAsync(CancellationToken cancellationToken)
{
if (_cmsHubConnection != null)
{
await _cmsHubConnection.DisposeAsync();
}
await base.StopAsync(cancellationToken);
}
}
/// <summary>
/// Notification payload for token revoked event (received from CMS)
/// </summary>
public class TokenRevokedNotification
{
public long UserId { get; set; }
public string Reason { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
/// <summary>
/// Notification payload for force refresh event (received from CMS)
/// </summary>
public class ForceRefreshNotification
{
public long UserId { get; set; }
public DateTime Timestamp { get; set; }
}
/// <summary>
/// Notification payload for broadcast message (received from CMS)
/// </summary>
public class BroadcastNotification
{
public string Message { get; set; } = string.Empty;
public DateTime Timestamp { get; set; }
}
@@ -0,0 +1,51 @@
using FrontOffice.BFF.Application.CityCQ.Queries.GetAllCitiesByFilter;
using FrontOffice.BFF.City.Protobuf;
using Mapster;
namespace FrontOffice.BFF.WebApi.Common.Mappings;
public class CityProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// Request: Proto → Application
config.NewConfig<GetAllCitiesByFilterRequest, GetAllCitiesByFilterQuery>()
.Map(dest => dest.PaginationState, src => src.PaginationState)
.Map(dest => dest.SortBy, src => src.SortBy)
.Map(dest => dest.Filter, src => src.Filter);
config.NewConfig<PaginationState, PaginationStateDto>()
.Map(dest => dest.PageNumber, src => src.PageNumber)
.Map(dest => dest.PageSize, src => src.PageSize);
config.NewConfig<GetAllCitiesByFilterFilter, GetAllCitiesByFilterFilterDto>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.Name, src => src.Name)
.Map(dest => dest.Native, src => src.Native)
.Map(dest => dest.StateId, src => src.StateId);
// Response: Application → Proto
config.NewConfig<GetAllCitiesByFilterResponseDto, GetAllCitiesByFilterResponse>()
.Map(dest => dest.MetaData, src => src.MetaData)
.Map(dest => dest.Models, src => src.Models);
config.NewConfig<MetaDataDto, MetaData>()
.Map(dest => dest.CurrentPage, src => src.CurrentPage)
.Map(dest => dest.TotalPage, src => src.TotalPage)
.Map(dest => dest.PageSize, src => src.PageSize)
.Map(dest => dest.TotalCount, src => src.TotalCount)
.Map(dest => dest.HasPrevious, src => src.HasPrevious)
.Map(dest => dest.HasNext, src => src.HasNext);
config.NewConfig<CityDto, GetAllCitiesByFilterResponseModel>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.ExternalId, src => src.ExternalId)
.Map(dest => dest.Name, src => src.Name)
.Map(dest => dest.Native, src => src.Native)
.Map(dest => dest.Latitude, src => src.Latitude)
.Map(dest => dest.Longitude, src => src.Longitude)
.Map(dest => dest.StateId, src => src.StateId)
.Map(dest => dest.StateName, src => src.StateName)
.Map(dest => dest.StateNative, src => src.StateNative);
}
}
@@ -1,5 +1,7 @@
using FrontOffice.BFF.Application.ClubMembershipCQ.Queries.GetMyClubMembership; using FrontOffice.BFF.Application.ClubMembershipCQ.Queries.GetMyClubMembership;
using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateMyClubMembership; using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateMyClubMembership;
using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.RequestClubContractOtp;
using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using ProtoDto = FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership; using ProtoDto = FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
@@ -38,5 +40,27 @@ public class ClubMembershipProfile : IRegister
.Map(dest => dest.ActivationDate, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.ActivationDate, DateTimeKind.Utc))) .Map(dest => dest.ActivationDate, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.ActivationDate, DateTimeKind.Utc)))
.Map(dest => dest.ExpirationDate, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.ExpirationDate, DateTimeKind.Utc))) .Map(dest => dest.ExpirationDate, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.ExpirationDate, DateTimeKind.Utc)))
.Map(dest => dest.AmountPaid, src => src.AmountPaid); .Map(dest => dest.AmountPaid, src => src.AmountPaid);
// RequestClubContractOtp mappings
config.NewConfig<ProtoDto.RequestClubContractOtpRequest, RequestClubContractOtpCommand>()
.Map(dest => dest.SignGuid, src => src.SignGuid);
config.NewConfig<RequestClubContractOtpResponseDto, ProtoDto.RequestClubContractOtpResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.RemainingAttempts, src => src.RemainingAttempts)
.Map(dest => dest.RemainingSeconds, src => src.RemainingSeconds);
// AcceptClubMembershipContract mappings
config.NewConfig<ProtoDto.AcceptClubMembershipContractRequest, AcceptClubMembershipContractCommand>()
.Map(dest => dest.OtpCode, src => src.OtpCode)
.Map(dest => dest.SignGuid, src => src.SignGuid)
.Map(dest => dest.ContractHtml, src => src.ContractHtml);
config.NewConfig<AcceptClubMembershipContractResponseDto, ProtoDto.AcceptClubMembershipContractResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.ContractId, src => src.ContractId)
.Map(dest => dest.Token, src => src.Token ?? "");
} }
} }
@@ -1,5 +1,6 @@
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts; using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWeeklyBalances; using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetWeekDefinitions;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using ProtoDto = FrontOffice.BFF.Commission.Protobuf.Protos.Commission; using ProtoDto = FrontOffice.BFF.Commission.Protobuf.Protos.Commission;
@@ -13,14 +14,17 @@ public class CommissionProfile : IRegister
config.NewConfig<ProtoDto.GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery>() config.NewConfig<ProtoDto.GetMyCommissionPayoutsRequest, GetMyCommissionPayoutsQuery>()
.Map(dest => dest.PageNumber, src => src.PageNumber) .Map(dest => dest.PageNumber, src => src.PageNumber)
.Map(dest => dest.PageSize, src => src.PageSize) .Map(dest => dest.PageSize, src => src.PageSize)
.Map(dest => dest.WeekNumber, src => src.WeekNumber) .Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId != null ? (long?)src.WeekDefinitionId.Value : null)
.Map(dest => dest.Status, src => src.Status); .Map(dest => dest.Status, src => src.Status);
config.NewConfig<ProtoDto.GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery>() config.NewConfig<ProtoDto.GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery>()
.Map(dest => dest.PageNumber, src => src.PageNumber) .MapWith(src => new GetMyWeeklyBalancesQuery
.Map(dest => dest.PageSize, src => src.PageSize) {
.Map(dest => dest.WeekNumber, src => src.WeekNumber) PageNumber = src.PageNumber,
.Map(dest => dest.OnlyActive, src => src.OnlyActive); PageSize = src.PageSize,
WeekDefinitionId = src.WeekDefinitionId != null ? src.WeekDefinitionId.Value : null,
OnlyActive = src.OnlyActive
});
// Response mappings // Response mappings
config.NewConfig<GetMyCommissionPayoutsResponseDto, ProtoDto.GetMyCommissionPayoutsResponse>() config.NewConfig<GetMyCommissionPayoutsResponseDto, ProtoDto.GetMyCommissionPayoutsResponse>()
@@ -29,13 +33,12 @@ public class CommissionProfile : IRegister
config.NewConfig<CommissionPayoutDto, ProtoDto.CommissionPayoutModel>() config.NewConfig<CommissionPayoutDto, ProtoDto.CommissionPayoutModel>()
.Map(dest => dest.Id, src => src.Id) .Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.WeekNumber, src => src.WeekNumber) .Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
.Map(dest => dest.WeekLabel, src => src.WeekLabel) .Map(dest => dest.WeekDisplayName, src => src.WeekDisplayName)
.Map(dest => dest.BalancesEarned, src => src.BalancesEarned) .Map(dest => dest.BalancesEarned, src => src.BalancesEarned)
.Map(dest => dest.TotalAmount, src => src.TotalAmount) .Map(dest => dest.TotalAmount, src => src.TotalAmount)
.Map(dest => dest.AmountFormatted, src => src.AmountFormatted) .Map(dest => dest.AmountFormatted, src => src.AmountFormatted)
.Map(dest => dest.Status, src => src.Status) .Map(dest => dest.Status, src => src.Status)
.Map(dest => dest.StatusBadgeColor, src => src.StatusBadgeColor)
.Map(dest => dest.CalculatedDate, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedDate, DateTimeKind.Utc))) .Map(dest => dest.CalculatedDate, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.CalculatedDate, DateTimeKind.Utc)))
.Map(dest => dest.DatePersian, src => src.DatePersian); .Map(dest => dest.DatePersian, src => src.DatePersian);
@@ -48,12 +51,42 @@ public class CommissionProfile : IRegister
config.NewConfig<WeeklyBalanceItemDto, ProtoDto.WeeklyBalanceModel>() config.NewConfig<WeeklyBalanceItemDto, ProtoDto.WeeklyBalanceModel>()
.Map(dest => dest.Id, src => src.Id) .Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.WeekNumber, src => src.WeekNumber) .Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
.Map(dest => dest.WeekDisplayName, src => src.WeekLabel)
.Map(dest => dest.LeftLegBalances, src => src.LeftLegBalances) .Map(dest => dest.LeftLegBalances, src => src.LeftLegBalances)
.Map(dest => dest.RightLegBalances, src => src.RightLegBalances) .Map(dest => dest.RightLegBalances, src => src.RightLegBalances)
.Map(dest => dest.LeftLegCarryover, src => src.LeftLegCarryover)
.Map(dest => dest.RightLegCarryover, src => src.RightLegCarryover)
.Map(dest => dest.LeftLegNewMembers, src => src.LeftLegNewMembers)
.Map(dest => dest.RightLegNewMembers, src => src.RightLegNewMembers)
.Map(dest => dest.TotalBalances, src => src.TotalBalances) .Map(dest => dest.TotalBalances, src => src.TotalBalances)
.Map(dest => dest.WeeklyPoolContribution, src => src.WeeklyPoolContribution) .Map(dest => dest.WeeklyPoolContribution, src => src.WeeklyPoolContribution)
.Map(dest => dest.IsExpired, src => src.IsExpired) .Map(dest => dest.IsExpired, src => src.IsExpired)
.Map(dest => dest.DatePersian, src => src.DatePersian); .Map(dest => dest.DatePersian, src => src.DatePersian);
// GetWeekDefinitions mappings
config.NewConfig<ProtoDto.GetWeekDefinitionsRequest, GetWeekDefinitionsQuery>()
.Map(dest => dest.SearchText, src => src.SearchText)
.Map(dest => dest.PageNumber, src => src.PageNumber > 0 ? src.PageNumber : 1)
.Map(dest => dest.PageSize, src => src.PageSize > 0 ? src.PageSize : 100)
.Map(dest => dest.GregorianYear, src => src.GregorianYear)
.Map(dest => dest.PersianYear, src => src.PersianYear)
.Map(dest => dest.IsActive, src => src.IsActive);
config.NewConfig<GetWeekDefinitionsResponseDto, ProtoDto.GetWeekDefinitionsResponse>()
.Map(dest => dest.TotalCount, src => src.TotalCount)
.Map(dest => dest.Data, src => src.Data);
config.NewConfig<WeekDefinitionDto, ProtoDto.WeekDefinitionModel>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.WeekOrder, src => src.WeekOrder)
.Map(dest => dest.DisplayName, src => src.DisplayName)
.Map(dest => dest.StartDate, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.StartDate, DateTimeKind.Utc)))
.Map(dest => dest.EndDate, src => Timestamp.FromDateTime(DateTime.SpecifyKind(src.EndDate, DateTimeKind.Utc)))
.Map(dest => dest.GregorianYear, src => src.GregorianYear)
.Map(dest => dest.PersianYear, src => src.PersianYear)
.Map(dest => dest.IsActive, src => src.IsActive)
.Map(dest => dest.IsCurrentWeek, src => src.IsCurrentWeek)
.Map(dest => dest.StartDatePersian, src => src.StartDatePersian)
.Map(dest => dest.EndDatePersian, src => src.EndDatePersian);
} }
} }
@@ -0,0 +1,38 @@
using FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubConfiguration;
using FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubFeatures;
using Google.Protobuf.WellKnownTypes;
using ProtoDto = FrontOffice.BFF.Configuration.Protobuf.Protos.Configuration;
namespace FrontOffice.BFF.WebApi.Common.Mappings;
public class ConfigurationProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// GetClubConfiguration mappings
config.NewConfig<GetClubConfigurationResponseDto, ProtoDto.GetClubConfigurationResponse>()
.Map(dest => dest.ActivationFee, src => src.ActivationFee)
.Map(dest => dest.MembershipGiftValue, src => src.MembershipGiftValue);
// GetClubFeatures mappings
config.NewConfig<GetClubFeaturesResponseDto, ProtoDto.GetClubFeaturesResponse>()
.MapWith(src => new ProtoDto.GetClubFeaturesResponse
{
Features = { src.Features.Select(f => f.Adapt<ProtoDto.ClubFeatureModel>()) }
});
config.NewConfig<ClubFeatureItemDto, ProtoDto.ClubFeatureModel>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.Title, src => src.Title)
.Map(dest => dest.Description, src => src.Description ?? "")
.Map(dest => dest.IsEnabled, src => src.IsEnabled)
.Map(dest => dest.DisplayOrder, src => src.DisplayOrder)
.Map(dest => dest.GrantedAt, src => src.GrantedAt != DateTime.MinValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.GrantedAt, DateTimeKind.Utc))
: null)
.Map(dest => dest.CreatedAt, src => src.CreatedAt != DateTime.MinValue
? Timestamp.FromDateTime(DateTime.SpecifyKind(src.CreatedAt, DateTimeKind.Utc))
: null)
.Map(dest => dest.Notes, src => src.Notes ?? "");
}
}
@@ -1,6 +1,7 @@
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
using Google.Protobuf.WellKnownTypes; using Google.Protobuf.WellKnownTypes;
using ProtoDto = FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership; using ProtoDto = FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
@@ -14,12 +15,22 @@ public class NetworkMembershipProfile : IRegister
config.NewConfig<ProtoDto.GetMyNetworkTreeRequest, GetMyNetworkTreeQuery>() config.NewConfig<ProtoDto.GetMyNetworkTreeRequest, GetMyNetworkTreeQuery>()
.Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3); .Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3);
config.NewConfig<ProtoDto.GetSubordinateTreeRequest, GetSubordinateTreeQuery>()
.Map(dest => dest.TargetUserId, src => src.TargetUserId)
.Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3);
// Response mappings - Tree // Response mappings - Tree
config.NewConfig<GetMyNetworkTreeResponseDto, ProtoDto.GetMyNetworkTreeResponse>() config.NewConfig<GetMyNetworkTreeResponseDto, ProtoDto.GetMyNetworkTreeResponse>()
.Map(dest => dest.RootNode, src => src.RootNode) .Map(dest => dest.RootNode, src => src.RootNode)
.Map(dest => dest.TotalMembers, src => src.TotalMembers) .Map(dest => dest.TotalMembers, src => src.TotalMembers)
.Map(dest => dest.CurrentDepth, src => src.CurrentDepth); .Map(dest => dest.CurrentDepth, src => src.CurrentDepth);
// Subordinate tree uses same response type
config.NewConfig<GetSubordinateTreeResponseDto, ProtoDto.GetMyNetworkTreeResponse>()
.Map(dest => dest.RootNode, src => src.RootNode)
.Map(dest => dest.TotalMembers, src => src.TotalMembers)
.Map(dest => dest.CurrentDepth, src => src.CurrentDepth);
config.NewConfig<NetworkNodeDto, ProtoDto.NetworkNodeModel>() config.NewConfig<NetworkNodeDto, ProtoDto.NetworkNodeModel>()
.Map(dest => dest.UserId, src => src.UserId) .Map(dest => dest.UserId, src => src.UserId)
.Map(dest => dest.FullName, src => src.FullName) .Map(dest => dest.FullName, src => src.FullName)
@@ -29,7 +40,12 @@ public class NetworkMembershipProfile : IRegister
.Map(dest => dest.LeftChild, src => src.LeftChild) .Map(dest => dest.LeftChild, src => src.LeftChild)
.Map(dest => dest.RightChild, src => src.RightChild) .Map(dest => dest.RightChild, src => src.RightChild)
.Map(dest => dest.Level, src => src.Level) .Map(dest => dest.Level, src => src.Level)
.Map(dest => dest.HasChildren, src => src.HasChildren); .Map(dest => dest.ReferralCode, src => src.ReferralCode)
.Map(dest => dest.HasChildren, src => src.HasChildren)
.Map(dest => dest.IsActive, src => src.IsActive)
.Map(dest => dest.JoinedAt, src => src.JoinedAt.HasValue ? Timestamp.FromDateTime(DateTime.SpecifyKind(src.JoinedAt.Value, DateTimeKind.Utc)) : null)
.Map(dest => dest.IsClubActive, src => src.IsClubActive)
.Map(dest => dest.ActivationWeekDefinitionId, src => src.ActivationWeekDefinitionId);
// Response mappings - Statistics // Response mappings - Statistics
config.NewConfig<GetMyNetworkStatisticsResponseDto, ProtoDto.GetMyNetworkStatisticsResponse>() config.NewConfig<GetMyNetworkStatisticsResponseDto, ProtoDto.GetMyNetworkStatisticsResponse>()
@@ -1,10 +1,41 @@
using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
using FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
namespace FrontOffice.BFF.WebApi.Common.Mappings; namespace FrontOffice.BFF.WebApi.Common.Mappings;
public class PackageProfile : IRegister public class PackageProfile : IRegister
{ {
void IRegister.Register(TypeAdapterConfig config) void IRegister.Register(TypeAdapterConfig config)
{ {
//config.NewConfig<Source,Destination>() // InitiateBasePackagePayment
// .Map(dest => dest.FullName, src => $"{src.Firstname} {src.Lastname}"); // UserId از CurrentUserService گرفته می‌شود، نه از Request
config.NewConfig<InitiateBasePackagePaymentRequest, InitiateBasePackagePaymentCommand>()
.Map(dest => dest.CallbackUrl, src => src.CallbackUrl);
config.NewConfig<InitiateBasePackagePaymentResponseDto, InitiateBasePackagePaymentResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.Amount, src => src.Amount)
.Map(dest => dest.PaymentGatewayUrl, src => src.PaymentGatewayUrl)
.Map(dest => dest.Authority, src => src.Authority);
// VerifyBasePackagePayment
config.NewConfig<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand>()
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.Authority, src => src.Authority)
.Map(dest => dest.Status, src => src.Status);
config.NewConfig<VerifyBasePackagePaymentResponseDto, VerifyBasePackagePaymentResponse>()
.Map(dest => dest.Success, src => src.Success)
.Map(dest => dest.Message, src => src.Message)
.Map(dest => dest.OrderId, src => src.OrderId)
.Map(dest => dest.TransactionId, src => src.TransactionId)
.Map(dest => dest.RefId, src => src.RefId)
.Map(dest => dest.WalletBalance, src => src.WalletBalance)
.Map(dest => dest.DiscountBalance, src => src.DiscountBalance);
} }
} }
@@ -1,4 +1,5 @@
using FrontOffice.BFF.Application.Common.Interfaces; using FrontOffice.BFF.Application.Common.Interfaces;
using FrontOffice.BFF.WebApi.BackgroundServices;
using FrontOffice.BFF.WebApi.Common.Services; using FrontOffice.BFF.WebApi.Common.Services;
using MapsterMapper; using MapsterMapper;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -15,6 +16,13 @@ public static class ConfigureServices
services.AddTransient<ICurrentUserService, CurrentUserService>(); services.AddTransient<ICurrentUserService, CurrentUserService>();
services.AddTransient<ITokenProvider, AppTokenProvider>(); services.AddTransient<ITokenProvider, AppTokenProvider>();
services.AddScoped<IDispatchRequestToCQRS, DispatchRequestToCQRS>(); services.AddScoped<IDispatchRequestToCQRS, DispatchRequestToCQRS>();
// Add SignalR services
services.AddSignalR();
// Add background service for CMS SignalR client
services.AddHostedService<CmsSignalRClientService>();
return services; return services;
} }
+20 -24
View File
@@ -1,44 +1,40 @@
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base FROM 194.5.195.53:32082/dotnet/aspnet:9.0 AS base
USER $APP_UID
WORKDIR /app WORKDIR /app
EXPOSE 8080 EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build FROM 194.5.195.53:32082/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src WORKDIR /src
COPY ["FrontOffice.BFF.WebApi/NuGet.config", "NuGet.config"]
# Copy main projects
COPY ["FrontOffice.BFF.WebApi/FrontOffice.BFF.WebApi.csproj", "FrontOffice.BFF.WebApi/"] COPY ["FrontOffice.BFF.WebApi/FrontOffice.BFF.WebApi.csproj", "FrontOffice.BFF.WebApi/"]
COPY ["FrontOffice.BFF.WebApi/NuGet.config", "FrontOffice.BFF.WebApi/"]
COPY ["FrontOffice.BFF.Application/FrontOffice.BFF.Application.csproj", "FrontOffice.BFF.Application/"]
COPY ["FrontOffice.BFF.Infrastructure/FrontOffice.BFF.Infrastructure.csproj", "FrontOffice.BFF.Infrastructure/"]
COPY ["FrontOffice.BFF.Domain/FrontOffice.BFF.Domain.csproj", "FrontOffice.BFF.Domain/"]
# Copy all Protobuf projects
COPY ["Protobufs/FrontOffice.BFF.Category.Protobuf/FrontOffice.BFF.Category.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Category.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.ClubMembership.Protobuf/FrontOffice.BFF.ClubMembership.Protobuf.csproj", "Protobufs/FrontOffice.BFF.ClubMembership.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.Commission.Protobuf/FrontOffice.BFF.Commission.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Commission.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.DiscountShop.Protobuf/FrontOffice.BFF.DiscountShop.Protobuf.csproj", "Protobufs/FrontOffice.BFF.DiscountShop.Protobuf/"] COPY ["Protobufs/FrontOffice.BFF.DiscountShop.Protobuf/FrontOffice.BFF.DiscountShop.Protobuf.csproj", "Protobufs/FrontOffice.BFF.DiscountShop.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/FrontOffice.BFF.NetworkMembership.Protobuf.csproj", "Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/"] COPY ["FrontOffice.BFF.Application/FrontOffice.BFF.Application.csproj", "FrontOffice.BFF.Application/"]
COPY ["Protobufs/FrontOffice.BFF.Package.Protobuf/FrontOffice.BFF.Package.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Package.Protobuf/"] COPY ["FrontOffice.BFF.Domain/FrontOffice.BFF.Domain.csproj", "FrontOffice.BFF.Domain/"]
COPY ["Protobufs/FrontOffice.BFF.UserOrder.Protobuf/FrontOffice.BFF.UserOrder.Protobuf.csproj", "Protobufs/FrontOffice.BFF.UserOrder.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.Category.Protobuf/FrontOffice.BFF.Category.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Category.Protobuf/"]
COPY ["FrontOffice.BFF.Infrastructure/FrontOffice.BFF.Infrastructure.csproj", "FrontOffice.BFF.Infrastructure/"]
COPY ["Protobufs/FrontOffice.BFF.Products.Protobuf/FrontOffice.BFF.Products.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Products.Protobuf/"] COPY ["Protobufs/FrontOffice.BFF.Products.Protobuf/FrontOffice.BFF.Products.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Products.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.ShopingCart.Protobuf/FrontOffice.BFF.ShopingCart.Protobuf.csproj", "Protobufs/FrontOffice.BFF.ShopingCart.Protobuf/"] COPY ["Protobufs/FrontOffice.BFF.ShopingCart.Protobuf/FrontOffice.BFF.ShopingCart.Protobuf.csproj", "Protobufs/FrontOffice.BFF.ShopingCart.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.Transaction.Protobuf/FrontOffice.BFF.Transaction.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Transaction.Protobuf/"] COPY ["Protobufs/FrontOffice.BFF.Transaction.Protobuf/FrontOffice.BFF.Transaction.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Transaction.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.UserWallet.Protobuf/FrontOffice.BFF.UserWallet.Protobuf.csproj", "Protobufs/FrontOffice.BFF.UserWallet.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.User.Protobuf/FrontOffice.BFF.User.Protobuf.csproj", "Protobufs/FrontOffice.BFF.User.Protobuf/"] COPY ["Protobufs/FrontOffice.BFF.User.Protobuf/FrontOffice.BFF.User.Protobuf.csproj", "Protobufs/FrontOffice.BFF.User.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.UserAddress.Protobuf/FrontOffice.BFF.UserAddress.Protobuf.csproj", "Protobufs/FrontOffice.BFF.UserAddress.Protobuf/"] COPY ["Protobufs/FrontOffice.BFF.UserAddress.Protobuf/FrontOffice.BFF.UserAddress.Protobuf.csproj", "Protobufs/FrontOffice.BFF.UserAddress.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.UserOrder.Protobuf/FrontOffice.BFF.UserOrder.Protobuf.csproj", "Protobufs/FrontOffice.BFF.UserOrder.Protobuf/"] COPY ["Protobufs/FrontOffice.BFF.Package.Protobuf/FrontOffice.BFF.Package.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Package.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.UserWallet.Protobuf/FrontOffice.BFF.UserWallet.Protobuf.csproj", "Protobufs/FrontOffice.BFF.UserWallet.Protobuf/"] COPY ["Protobufs/FrontOffice.BFF.Commission.Protobuf/FrontOffice.BFF.Commission.Protobuf.csproj", "Protobufs/FrontOffice.BFF.Commission.Protobuf/"]
COPY ["Protobufs/FrontOffice.BFF.ClubMembership.Protobuf/FrontOffice.BFF.ClubMembership.Protobuf.csproj", "Protobufs/FrontOffice.BFF.ClubMembership.Protobuf/"]
RUN dotnet restore "FrontOffice.BFF.WebApi/FrontOffice.BFF.WebApi.csproj" --configfile "FrontOffice.BFF.WebApi/NuGet.config" COPY ["Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/FrontOffice.BFF.NetworkMembership.Protobuf.csproj", "Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/"]
RUN dotnet restore "FrontOffice.BFF.WebApi/FrontOffice.BFF.WebApi.csproj" --configfile NuGet.config
COPY . . COPY . .
WORKDIR "/src/FrontOffice.BFF.WebApi" WORKDIR "/src/FrontOffice.BFF.WebApi"
RUN dotnet build "FrontOffice.BFF.WebApi.csproj" -c Release -o /app/build RUN dotnet build "./FrontOffice.BFF.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish FROM build AS publish
RUN dotnet publish "FrontOffice.BFF.WebApi.csproj" -c Release -o /app/publish /p:UseAppHost=false ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./FrontOffice.BFF.WebApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "FrontOffice.BFF.WebApi.dll"] ENTRYPOINT ["dotnet", "FrontOffice.BFF.WebApi.dll"]
@@ -15,6 +15,8 @@
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="9.0.2" /> <PackageReference Include="Serilog.Sinks.MSSqlServer" Version="9.0.2" />
<PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" /> <PackageReference Include="Serilog.Sinks.Seq" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -34,6 +36,13 @@
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.DiscountShop.Protobuf\FrontOffice.BFF.DiscountShop.Protobuf.csproj" /> <ProjectReference Include="..\Protobufs\FrontOffice.BFF.DiscountShop.Protobuf\FrontOffice.BFF.DiscountShop.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.Commission.Protobuf\FrontOffice.BFF.Commission.Protobuf.csproj" /> <ProjectReference Include="..\Protobufs\FrontOffice.BFF.Commission.Protobuf\FrontOffice.BFF.Commission.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.ClubMembership.Protobuf\FrontOffice.BFF.ClubMembership.Protobuf.csproj" /> <ProjectReference Include="..\Protobufs\FrontOffice.BFF.ClubMembership.Protobuf\FrontOffice.BFF.ClubMembership.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.Configuration.Protobuf\FrontOffice.BFF.Configuration.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.Protobuf.csproj" /> <ProjectReference Include="..\Protobufs\FrontOffice.BFF.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\FrontOffice.BFF.City.Protobuf\FrontOffice.BFF.City.Protobuf.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\.dockerignore">
<Link>.dockerignore</Link>
</Content>
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
namespace FrontOffice.BFF.WebApi.Hubs;
/// <summary>
/// SignalR Hub for relaying token notifications from CMS to Frontend clients.
/// This hub is used by Frontend to receive token revocation/refresh notifications.
/// </summary>
[Authorize(Roles = "user")]
public class TokenRelayHub : Hub
{
private readonly ILogger<TokenRelayHub> _logger;
public TokenRelayHub(ILogger<TokenRelayHub> logger)
{
_logger = logger;
}
public override async Task OnConnectedAsync()
{
var userId = Context.User?.FindFirst("userId")?.Value;
if (!string.IsNullOrEmpty(userId))
{
// Subscribe this connection to the user's group for notifications
await Groups.AddToGroupAsync(Context.ConnectionId, $"user_{userId}");
_logger.LogInformation("Frontend client connected to TokenRelayHub: {ConnectionId}, UserId: {UserId}",
Context.ConnectionId, userId);
}
else
{
_logger.LogWarning("Frontend client connected without userId: {ConnectionId}", Context.ConnectionId);
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = Context.User?.FindFirst("userId")?.Value;
if (!string.IsNullOrEmpty(userId))
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user_{userId}");
}
_logger.LogInformation("Frontend client disconnected from TokenRelayHub: {ConnectionId}, Exception: {Exception}",
Context.ConnectionId, exception?.Message);
await base.OnDisconnectedAsync(exception);
}
}
+7 -2
View File
@@ -1,5 +1,6 @@
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using FrontOffice.BFF.WebApi.Hubs;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -13,14 +14,14 @@ if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
builder.WebHost.ConfigureKestrel(options => builder.WebHost.ConfigureKestrel(options =>
{ {
// Setup a HTTP/2 endpoint without TLS. // Setup a HTTP/2 endpoint without TLS.
options.ListenLocalhost(5000, o => o.Protocols = options.ListenLocalhost(5002, o => o.Protocols =
HttpProtocols.Http2); HttpProtocols.Http2);
}); });
} }
var levelSwitch = new LoggingLevelSwitch(); var levelSwitch = new LoggingLevelSwitch();
var logger = new LoggerConfiguration() var logger = new LoggerConfiguration()
//.WriteTo.Console() .WriteTo.Console()
//.WriteTo.MSSqlServer(builder.Configuration.GetConnectionString("LogConnection"), //.WriteTo.MSSqlServer(builder.Configuration.GetConnectionString("LogConnection"),
// sinkOptions: new MSSqlServerSinkOptions // sinkOptions: new MSSqlServerSinkOptions
// { // {
@@ -94,6 +95,10 @@ app.UseCors("AllowAll");
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); // Configure the HTTP request pipeline. app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); // Configure the HTTP request pipeline.
// Map SignalR Hub for token notifications to Frontend clients
app.MapHub<TokenRelayHub>("/hubs/token-relay");
app.ConfigureGrpcEndpoints(Assembly.GetExecutingAssembly(), endpoints => app.ConfigureGrpcEndpoints(Assembly.GetExecutingAssembly(), endpoints =>
{ {
// endpoints.MapGrpcService<ProductService>(); // endpoints.MapGrpcService<ProductService>();
@@ -0,0 +1,26 @@
using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetAppVersion;
using FrontOffice.BFF.Configuration.Protobuf.Protos.AppVersion;
namespace FrontOffice.BFF.WebApi.Services;
/// <summary>
/// سرویس نسخه اپلیکیشن - برای بررسی نیاز به پاک کردن کش
/// </summary>
public class AppVersionGrpcService : AppVersionContract.AppVersionContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public AppVersionGrpcService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
/// <summary>
/// دریافت نسخه اپلیکیشن
/// </summary>
public override async Task<GetAppVersionResponse> GetAppVersion(GetAppVersionRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetAppVersionRequest, GetAppVersionQuery, GetAppVersionResponse>(request, context);
}
}
@@ -0,0 +1,25 @@
using FrontOffice.BFF.Application.CityCQ.Queries.GetAllCitiesByFilter;
using FrontOffice.BFF.City.Protobuf;
using FrontOffice.BFF.WebApi.Common.Services;
namespace FrontOffice.BFF.WebApi.Services;
public class CityService : CityContract.CityContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public CityService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
public override async Task<GetAllCitiesByFilterResponse> GetAllCitiesByFilter(
GetAllCitiesByFilterRequest request,
ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<
GetAllCitiesByFilterRequest,
GetAllCitiesByFilterQuery,
GetAllCitiesByFilterResponse>(request, context);
}
}
@@ -1,6 +1,8 @@
using FrontOffice.BFF.WebApi.Common.Services; using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.ClubMembershipCQ.Queries.GetMyClubMembership; using FrontOffice.BFF.Application.ClubMembershipCQ.Queries.GetMyClubMembership;
using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateMyClubMembership; using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateMyClubMembership;
using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.RequestClubContractOtp;
using FrontOffice.BFF.Application.ClubMembershipCQ.Commands.AcceptClubMembershipContract;
using FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership; using FrontOffice.BFF.ClubMembership.Protobuf.Protos.ClubMembership;
namespace FrontOffice.BFF.WebApi.Services; namespace FrontOffice.BFF.WebApi.Services;
@@ -32,4 +34,20 @@ public class ClubMembershipGrpcService : ClubMembershipContract.ClubMembershipCo
{ {
return await _dispatchRequestToCQRS.Handle<ActivateMyClubMembershipRequest, ActivateMyClubMembershipCommand, ActivateMyClubMembershipResponse>(request, context); return await _dispatchRequestToCQRS.Handle<ActivateMyClubMembershipRequest, ActivateMyClubMembershipCommand, ActivateMyClubMembershipResponse>(request, context);
} }
/// <summary>
/// ارسال OTP برای امضای قرارداد باشگاه مشتریان
/// </summary>
public override async Task<RequestClubContractOtpResponse> RequestClubContractOtp(RequestClubContractOtpRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<RequestClubContractOtpRequest, RequestClubContractOtpCommand, RequestClubContractOtpResponse>(request, context);
}
/// <summary>
/// امضای قرارداد باشگاه مشتریان و فعال‌سازی
/// </summary>
public override async Task<AcceptClubMembershipContractResponse> AcceptClubMembershipContract(AcceptClubMembershipContractRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<AcceptClubMembershipContractRequest, AcceptClubMembershipContractCommand, AcceptClubMembershipContractResponse>(request, context);
}
} }
@@ -1,6 +1,7 @@
using FrontOffice.BFF.WebApi.Common.Services; using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts; using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyCommissionPayouts;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWeeklyBalances; using FrontOffice.BFF.Application.CommissionCQ.Queries.GetMyWeeklyBalances;
using FrontOffice.BFF.Application.CommissionCQ.Queries.GetWeekDefinitions;
using FrontOffice.BFF.Commission.Protobuf.Protos.Commission; using FrontOffice.BFF.Commission.Protobuf.Protos.Commission;
namespace FrontOffice.BFF.WebApi.Services; namespace FrontOffice.BFF.WebApi.Services;
@@ -32,4 +33,12 @@ public class CommissionService : CommissionContract.CommissionContractBase
{ {
return await _dispatchRequestToCQRS.Handle<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery, GetMyWeeklyBalancesResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetMyWeeklyBalancesRequest, GetMyWeeklyBalancesQuery, GetMyWeeklyBalancesResponse>(request, context);
} }
/// <summary>
/// دریافت لیست هفته‌ها برای dropdown
/// </summary>
public override async Task<GetWeekDefinitionsResponse> GetWeekDefinitions(GetWeekDefinitionsRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetWeekDefinitionsRequest, GetWeekDefinitionsQuery, GetWeekDefinitionsResponse>(request, context);
}
} }
@@ -0,0 +1,35 @@
using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubConfiguration;
using FrontOffice.BFF.Application.ConfigurationCQ.Queries.GetClubFeatures;
using FrontOffice.BFF.Configuration.Protobuf.Protos.Configuration;
namespace FrontOffice.BFF.WebApi.Services;
/// <summary>
/// سرویس تنظیمات - برای کاربران FrontOffice
/// </summary>
public class ConfigurationGrpcService : ConfigurationContract.ConfigurationContractBase
{
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
public ConfigurationGrpcService(IDispatchRequestToCQRS dispatchRequestToCQRS)
{
_dispatchRequestToCQRS = dispatchRequestToCQRS;
}
/// <summary>
/// دریافت تنظیمات باشگاه مشتریان
/// </summary>
public override async Task<GetClubConfigurationResponse> GetClubConfiguration(Empty request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetClubConfigurationQuery, GetClubConfigurationResponse>(context);
}
/// <summary>
/// دریافت لیست فیچرهای باشگاه مشتریان
/// </summary>
public override async Task<GetClubFeaturesResponse> GetClubFeatures(Empty request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetClubFeaturesQuery, GetClubFeaturesResponse>(context);
}
}
@@ -2,6 +2,7 @@ using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition;
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership; using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership;
namespace FrontOffice.BFF.WebApi.Services; namespace FrontOffice.BFF.WebApi.Services;
@@ -26,6 +27,14 @@ public class NetworkMembershipService : NetworkMembershipContract.NetworkMembers
return await _dispatchRequestToCQRS.Handle<GetMyNetworkTreeRequest, GetMyNetworkTreeQuery, GetMyNetworkTreeResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetMyNetworkTreeRequest, GetMyNetworkTreeQuery, GetMyNetworkTreeResponse>(request, context);
} }
/// <summary>
/// دریافت درخت شبکه یک زیرمجموعه (با چک امنیتی)
/// </summary>
public override async Task<GetMyNetworkTreeResponse> GetSubordinateTree(GetSubordinateTreeRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<GetSubordinateTreeRequest, GetSubordinateTreeQuery, GetMyNetworkTreeResponse>(request, context);
}
/// <summary> /// <summary>
/// دریافت آمار شبکه کاربر جاری /// دریافت آمار شبکه کاربر جاری
/// </summary> /// </summary>
@@ -2,6 +2,9 @@ using FrontOffice.BFF.Package.Protobuf.Protos.Package;
using FrontOffice.BFF.WebApi.Common.Services; using FrontOffice.BFF.WebApi.Common.Services;
using FrontOffice.BFF.Application.PackageCQ.Queries.GetPackage; using FrontOffice.BFF.Application.PackageCQ.Queries.GetPackage;
using FrontOffice.BFF.Application.PackageCQ.Queries.GetAllPackageByFilter; using FrontOffice.BFF.Application.PackageCQ.Queries.GetAllPackageByFilter;
using FrontOffice.BFF.Application.PackageCQ.Commands.InitiateBasePackagePayment;
using FrontOffice.BFF.Application.PackageCQ.Commands.VerifyBasePackagePayment;
namespace FrontOffice.BFF.WebApi.Services; namespace FrontOffice.BFF.WebApi.Services;
public class PackageService : PackageContract.PackageContractBase public class PackageService : PackageContract.PackageContractBase
{ {
@@ -19,4 +22,15 @@ public class PackageService : PackageContract.PackageContractBase
{ {
return await _dispatchRequestToCQRS.Handle<GetAllPackageByFilterRequest, GetAllPackageByFilterQuery, GetAllPackageByFilterResponse>(request, context); return await _dispatchRequestToCQRS.Handle<GetAllPackageByFilterRequest, GetAllPackageByFilterQuery, GetAllPackageByFilterResponse>(request, context);
} }
// Base Package Payment (56M پکیج پایه)
public override async Task<InitiateBasePackagePaymentResponse> InitiateBasePackagePayment(InitiateBasePackagePaymentRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<InitiateBasePackagePaymentRequest, InitiateBasePackagePaymentCommand, InitiateBasePackagePaymentResponse>(request, context);
}
public override async Task<VerifyBasePackagePaymentResponse> VerifyBasePackagePayment(VerifyBasePackagePaymentRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<VerifyBasePackagePaymentRequest, VerifyBasePackagePaymentCommand, VerifyBasePackagePaymentResponse>(request, context);
}
} }
@@ -4,17 +4,22 @@ using FrontOffice.BFF.Application.UserOrderCQ.Commands.UpdateUserOrder;
using FrontOffice.BFF.Application.UserOrderCQ.Commands.DeleteUserOrder; using FrontOffice.BFF.Application.UserOrderCQ.Commands.DeleteUserOrder;
using FrontOffice.BFF.Application.UserOrderCQ.Queries.GetUserOrder; using FrontOffice.BFF.Application.UserOrderCQ.Queries.GetUserOrder;
using FrontOffice.BFF.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter; using FrontOffice.BFF.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
using FrontOffice.BFF.Application.UserOrderCQ.Queries.GetVATRate;
using FrontOffice.BFF.Application.UserOrderCQ.Commands.SubmitShopBuyOrder; using FrontOffice.BFF.Application.UserOrderCQ.Commands.SubmitShopBuyOrder;
using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder; using FrontOffice.BFF.UserOrder.Protobuf.Protos.UserOrder;
using MediatR;
using Mapster;
namespace FrontOffice.BFF.WebApi.Services; namespace FrontOffice.BFF.WebApi.Services;
public class UserOrderService : UserOrderContract.UserOrderContractBase public class UserOrderService : UserOrderContract.UserOrderContractBase
{ {
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
private readonly ISender _mediator;
public UserOrderService(IDispatchRequestToCQRS dispatchRequestToCQRS) public UserOrderService(IDispatchRequestToCQRS dispatchRequestToCQRS, ISender mediator)
{ {
_dispatchRequestToCQRS = dispatchRequestToCQRS; _dispatchRequestToCQRS = dispatchRequestToCQRS;
_mediator = mediator;
} }
public override async Task<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context) public override async Task<CreateNewUserOrderResponse> CreateNewUserOrder(CreateNewUserOrderRequest request, ServerCallContext context)
{ {
@@ -40,4 +45,10 @@ public class UserOrderService : UserOrderContract.UserOrderContractBase
{ {
return await _dispatchRequestToCQRS.Handle<SubmitShopBuyOrderRequest, SubmitShopBuyOrderCommand, SubmitShopBuyOrderResponse>(request, context); return await _dispatchRequestToCQRS.Handle<SubmitShopBuyOrderRequest, SubmitShopBuyOrderCommand, SubmitShopBuyOrderResponse>(request, context);
} }
public override async Task<GetVATRateResponse> GetVATRate(Empty request, ServerCallContext context)
{
var result = await _mediator.Send(new GetVATRateQuery());
return result.Adapt<GetVATRateResponse>();
}
} }
@@ -8,6 +8,7 @@ using FrontOffice.BFF.Application.UserCQ.Commands.CreateNewOtpToken;
using FrontOffice.BFF.Application.UserCQ.Commands.VerifyOtpToken; using FrontOffice.BFF.Application.UserCQ.Commands.VerifyOtpToken;
using FrontOffice.BFF.Application.UserCQ.Queries.AdminGetJwtToken; using FrontOffice.BFF.Application.UserCQ.Queries.AdminGetJwtToken;
using FrontOffice.BFF.Application.UserCQ.Commands.SetPasswordForUser; using FrontOffice.BFF.Application.UserCQ.Commands.SetPasswordForUser;
using FrontOffice.BFF.Application.UserCQ.Commands.RefreshToken;
using FrontOffice.BFF.User.Protobuf.Protos.User; using FrontOffice.BFF.User.Protobuf.Protos.User;
namespace FrontOffice.BFF.WebApi.Services; namespace FrontOffice.BFF.WebApi.Services;
@@ -59,4 +60,10 @@ public class UserService : UserContract.UserContractBase
{ {
return await _dispatchRequestToCQRS.Handle<AcceptContractRequest, AcceptContractCommand, AcceptContractRequestResponse>(request, context); return await _dispatchRequestToCQRS.Handle<AcceptContractRequest, AcceptContractCommand, AcceptContractRequestResponse>(request, context);
} }
[Authorize(Roles = "user")]
public override async Task<RefreshTokenResponse> RefreshToken(RefreshTokenRequest request, ServerCallContext context)
{
return await _dispatchRequestToCQRS.Handle<RefreshTokenRequest, RefreshTokenCommand, RefreshTokenResponse>(request, context);
}
} }
@@ -30,7 +30,7 @@ public class UserWalletService : UserWalletContract.UserWalletContractBase
} }
public override async Task<Empty> WithdrawBalance(WithdrawBalanceRequest request, ServerCallContext context) public override async Task<Empty> WithdrawBalance(WithdrawBalanceRequest request, ServerCallContext context)
{ {
return await _dispatchRequestToCQRS.Handle<WithdrawBalanceRequest, WithdrawBalanceCommand, Empty>(request, context); return await _dispatchRequestToCQRS.Handle<WithdrawBalanceRequest, WithdrawBalanceCommand>(request, context);
} }
public override async Task<GetUserWithdrawalsResponse> GetUserWithdrawals(GetUserWithdrawalsRequest request, ServerCallContext context) public override async Task<GetUserWithdrawalsResponse> GetUserWithdrawals(GetUserWithdrawalsRequest request, ServerCallContext context)
{ {
@@ -10,11 +10,18 @@
} }
}, },
"GrpcChannelOptions": { "GrpcChannelOptions": {
"CMSMSAddress": "https://cms.kbs1.ir", "CMSMSAddress": "http://cms-svc",
"PYMSMSAddress": "https://ipg.afrino.co" "PYMSMSAddress": "https://ipg.afrino.co"
}, },
"CmsSignalR": {
"HubPath": "/hubs/token-notification"
},
"Authentication": { "Authentication": {
"Authority": "https://ids.domain.com/", "Authority": "https://ids.domain.com/",
"Audience": "domain_api" "Audience": "domain_api"
},
"Seq": {
"ServerUrl": "http://seq-svc:5341",
"ApiKey": ""
} }
} }
@@ -0,0 +1,27 @@
{
"JwtSecurityKey": "TvlZVx5TJaHs8e9HgUdGzhGP2CIidoI444nAj+8+g7c=",
"JwtIssuer": "https://localhost",
"JwtAudience": "https://localhost",
"JwtExpiryInDays": 365,
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
},
"GrpcChannelOptions": {
"CMSMSAddress": "http://cms-svc",
"PYMSMSAddress": "https://ipg.afrino.co"
},
"CmsSignalR": {
"HubPath": "/hubs/token-notification"
},
"Authentication": {
"Authority": "https://ids.domain.com/",
"Audience": "domain_api"
},
"Seq": {
"ServerUrl": "http://seq-svc:5341",
"ApiKey": ""
}
}
+4 -1
View File
@@ -10,9 +10,12 @@
} }
}, },
"GrpcChannelOptions": { "GrpcChannelOptions": {
"CMSMSAddress": "https://cms.kbs1.ir", "CMSMSAddress": "https://cms.foursat.afrino.co",
"PYMSMSAddress": "https://ipg.afrino.co" "PYMSMSAddress": "https://ipg.afrino.co"
}, },
"CmsSignalR": {
"HubPath": "/hubs/token-notification"
},
"Authentication": { "Authentication": {
"Authority": "https://ids.domain.com/", "Authority": "https://ids.domain.com/",
"Audience": "domain_api" "Audience": "domain_api"
+30
View File
@@ -39,6 +39,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.ClubMembers
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.NetworkMembership.Protobuf", "Protobufs\FrontOffice.BFF.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.Protobuf.csproj", "{CCA23A57-4BC4-4C53-9A96-41FCFF5407F5}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.NetworkMembership.Protobuf", "Protobufs\FrontOffice.BFF.NetworkMembership.Protobuf\FrontOffice.BFF.NetworkMembership.Protobuf.csproj", "{CCA23A57-4BC4-4C53-9A96-41FCFF5407F5}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.City.Protobuf", "Protobufs\FrontOffice.BFF.City.Protobuf\FrontOffice.BFF.City.Protobuf.csproj", "{41403EBB-E67B-413B-8B7A-64D67E221984}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FrontOffice.BFF.Configuration.Protobuf", "Protobufs\FrontOffice.BFF.Configuration.Protobuf\FrontOffice.BFF.Configuration.Protobuf.csproj", "{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -253,6 +257,30 @@ Global
{CCA23A57-4BC4-4C53-9A96-41FCFF5407F5}.Release|x64.Build.0 = Release|Any CPU {CCA23A57-4BC4-4C53-9A96-41FCFF5407F5}.Release|x64.Build.0 = Release|Any CPU
{CCA23A57-4BC4-4C53-9A96-41FCFF5407F5}.Release|x86.ActiveCfg = Release|Any CPU {CCA23A57-4BC4-4C53-9A96-41FCFF5407F5}.Release|x86.ActiveCfg = Release|Any CPU
{CCA23A57-4BC4-4C53-9A96-41FCFF5407F5}.Release|x86.Build.0 = Release|Any CPU {CCA23A57-4BC4-4C53-9A96-41FCFF5407F5}.Release|x86.Build.0 = Release|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Debug|x64.ActiveCfg = Debug|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Debug|x64.Build.0 = Debug|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Debug|x86.ActiveCfg = Debug|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Debug|x86.Build.0 = Debug|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Release|Any CPU.Build.0 = Release|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Release|x64.ActiveCfg = Release|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Release|x64.Build.0 = Release|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Release|x86.ActiveCfg = Release|Any CPU
{41403EBB-E67B-413B-8B7A-64D67E221984}.Release|x86.Build.0 = Release|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Debug|x64.ActiveCfg = Debug|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Debug|x64.Build.0 = Debug|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Debug|x86.ActiveCfg = Debug|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Debug|x86.Build.0 = Debug|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Release|Any CPU.Build.0 = Release|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Release|x64.ActiveCfg = Release|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Release|x64.Build.0 = Release|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Release|x86.ActiveCfg = Release|Any CPU
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -271,5 +299,7 @@ Global
{B1380466-18E7-4CAD-88F8-E1419D2B6300} = {CA9BF4D6-6729-4011-888E-48F5F739B469} {B1380466-18E7-4CAD-88F8-E1419D2B6300} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
{B6EAE0A3-3427-4D86-B2BA-B185F476B74F} = {CA9BF4D6-6729-4011-888E-48F5F739B469} {B6EAE0A3-3427-4D86-B2BA-B185F476B74F} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
{CCA23A57-4BC4-4C53-9A96-41FCFF5407F5} = {CA9BF4D6-6729-4011-888E-48F5F739B469} {CCA23A57-4BC4-4C53-9A96-41FCFF5407F5} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
{41403EBB-E67B-413B-8B7A-64D67E221984} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
{4FFEE929-9DFF-4C60-A22C-A75DD44D2A01} = {CA9BF4D6-6729-4011-888E-48F5F739B469}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
@@ -3,7 +3,7 @@
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.12</Version> <Version>0.0.14</Version>
<PackageId>Foursat.FrontOffice.BFF.Category.Protobuf</PackageId> <PackageId>Foursat.FrontOffice.BFF.Category.Protobuf</PackageId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild> <GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
@@ -23,12 +23,15 @@
<Protobuf Include="Protos\category.proto" ProtoRoot="Protos\" GrpcServices="Both" /> <Protobuf Include="Protos\category.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup> </ItemGroup>
<Target Name="PushToFoursatNuget" AfterTargets="Pack"> <Target Name="PushToFourSat" AfterTargets="Pack">
<PropertyGroup> <PropertyGroup>
<NugetPackagePath>$(PackageOutputPath)$(PackageId).$(Version).nupkg</NugetPackagePath> <NugetPackagePath>$(PackageOutputPath)$(PackageId).$(Version).nupkg</NugetPackagePath>
<PushCommand>dotnet nuget push **/*.nupkg --source https://git.afrino.co/api/packages/FourSat/nuget/index.json --api-key 061a5cb15517c6da39c16cfce8556c55ae104d0d --skip-duplicate &amp;&amp; del "$(NugetPackagePath)"</PushCommand> <PushCommand>
dotnet nuget push **/*.nupkg --source https://git.afrino.co/api/packages/FourSat/nuget/index.json --api-key 061a5cb15517c6da39c16cfce8556c55ae104d0d --skip-duplicate
</PushCommand>
</PropertyGroup> </PropertyGroup>
<Exec Command="$(PushCommand)" /> <Exec Command="$(PushCommand)" />
</Target> </Target>
</Project> </Project>
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.2</Version>
<PackageId>Foursat.FrontOffice.BFF.City.Protobuf</PackageId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<DebugSymbols>False</DebugSymbols>
<DebugType>None</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.23.3" />
<PackageReference Include="Grpc.Core.Api" Version="2.54.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.2.2" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.10.0" />
<PackageReference Include="Grpc.Tools" Version="2.55.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\city.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup>
<Target Name="PushToFourSat" AfterTargets="Pack">
<PropertyGroup>
<NugetPackagePath>$(PackageOutputPath)$(PackageId).$(Version).nupkg</NugetPackagePath>
<PushCommand>
dotnet nuget push **/*.nupkg --source https://git.afrino.co/api/packages/FourSat/nuget/index.json --api-key 061a5cb15517c6da39c16cfce8556c55ae104d0d --skip-duplicate
</PushCommand>
</PropertyGroup>
<Exec Command="$(PushCommand)" />
</Target>
</Project>
@@ -0,0 +1,62 @@
syntax = "proto3";
package city;
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option csharp_namespace = "FrontOffice.BFF.City.Protobuf";
service CityContract {
rpc GetAllCitiesByFilter(GetAllCitiesByFilterRequest) returns (GetAllCitiesByFilterResponse){
option (google.api.http) = {
get: "/GetAllCitiesByFilter"
};
};
}
message GetAllCitiesByFilterRequest {
PaginationState pagination_state = 1;
google.protobuf.StringValue sort_by = 2;
GetAllCitiesByFilterFilter filter = 3;
}
message GetAllCitiesByFilterFilter {
google.protobuf.Int64Value id = 1;
google.protobuf.StringValue name = 2;
google.protobuf.StringValue native = 3;
google.protobuf.Int64Value state_id = 4;
}
message GetAllCitiesByFilterResponse {
MetaData meta_data = 1;
repeated GetAllCitiesByFilterResponseModel models = 2;
}
message GetAllCitiesByFilterResponseModel {
int64 id = 1;
int64 external_id = 2;
string name = 3;
string native = 4;
string latitude = 5;
string longitude = 6;
int64 state_id = 7;
string state_name = 8;
string state_native = 9;
}
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;
}
@@ -0,0 +1,31 @@
// Copyright (c) 2015, Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
import "google/api/http.proto";
import "google/protobuf/descriptor.proto";
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
extend google.protobuf.MethodOptions {
// See `HttpRule`.
HttpRule http = 72295728;
}
@@ -0,0 +1,377 @@
// Copyright 2019 Google LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
syntax = "proto3";
package google.api;
option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
// Defines the HTTP configuration for an API service. It contains a list of
// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
// to one or more HTTP REST API methods.
message Http {
// A list of HTTP configuration rules that apply to individual API methods.
//
// **NOTE:** All service configuration rules follow "last one wins" order.
repeated HttpRule rules = 1;
// When set to true, URL path parameters will be fully URI-decoded except in
// cases of single segment matches in reserved expansion, where "%2F" will be
// left encoded.
//
// The default behavior is to not decode RFC 6570 reserved characters in multi
// segment matches.
bool fully_decode_reserved_expansion = 2;
}
// # gRPC Transcoding
//
// gRPC Transcoding is a feature for mapping between a gRPC method and one or
// more HTTP REST endpoints. It allows developers to build a single API service
// that supports both gRPC APIs and REST APIs. Many systems, including [Google
// APIs](https://github.com/googleapis/googleapis),
// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC
// Gateway](https://github.com/grpc-ecosystem/grpc-gateway),
// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature
// and use it for large scale production services.
//
// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies
// how different portions of the gRPC request message are mapped to the URL
// path, URL query parameters, and HTTP request body. It also controls how the
// gRPC response message is mapped to the HTTP response body. `HttpRule` is
// typically specified as an `google.api.http` annotation on the gRPC method.
//
// Each mapping specifies a URL path template and an HTTP method. The path
// template may refer to one or more fields in the gRPC request message, as long
// as each field is a non-repeated field with a primitive (non-message) type.
// The path template controls how fields of the request message are mapped to
// the URL path.
//
// Example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get: "/v1/{name=messages/*}"
// };
// }
// }
// message GetMessageRequest {
// string name = 1; // Mapped to URL path.
// }
// message Message {
// string text = 1; // The resource content.
// }
//
// This enables an HTTP REST to gRPC mapping as below:
//
// HTTP | gRPC
// -----|-----
// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")`
//
// Any fields in the request message which are not bound by the path template
// automatically become HTTP query parameters if there is no HTTP request body.
// For example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get:"/v1/messages/{message_id}"
// };
// }
// }
// message GetMessageRequest {
// message SubMessage {
// string subfield = 1;
// }
// string message_id = 1; // Mapped to URL path.
// int64 revision = 2; // Mapped to URL query parameter `revision`.
// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`.
// }
//
// This enables a HTTP JSON to RPC mapping as below:
//
// HTTP | gRPC
// -----|-----
// `GET /v1/messages/123456?revision=2&sub.subfield=foo` |
// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield:
// "foo"))`
//
// Note that fields which are mapped to URL query parameters must have a
// primitive type or a repeated primitive type or a non-repeated message type.
// In the case of a repeated type, the parameter can be repeated in the URL
// as `...?param=A&param=B`. In the case of a message type, each field of the
// message is mapped to a separate parameter, such as
// `...?foo.a=A&foo.b=B&foo.c=C`.
//
// For HTTP methods that allow a request body, the `body` field
// specifies the mapping. Consider a REST update method on the
// message resource collection:
//
// service Messaging {
// rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
// option (google.api.http) = {
// patch: "/v1/messages/{message_id}"
// body: "message"
// };
// }
// }
// message UpdateMessageRequest {
// string message_id = 1; // mapped to the URL
// Message message = 2; // mapped to the body
// }
//
// The following HTTP JSON to RPC mapping is enabled, where the
// representation of the JSON in the request body is determined by
// protos JSON encoding:
//
// HTTP | gRPC
// -----|-----
// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id:
// "123456" message { text: "Hi!" })`
//
// The special name `*` can be used in the body mapping to define that
// every field not bound by the path template should be mapped to the
// request body. This enables the following alternative definition of
// the update method:
//
// service Messaging {
// rpc UpdateMessage(Message) returns (Message) {
// option (google.api.http) = {
// patch: "/v1/messages/{message_id}"
// body: "*"
// };
// }
// }
// message Message {
// string message_id = 1;
// string text = 2;
// }
//
//
// The following HTTP JSON to RPC mapping is enabled:
//
// HTTP | gRPC
// -----|-----
// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id:
// "123456" text: "Hi!")`
//
// Note that when using `*` in the body mapping, it is not possible to
// have HTTP parameters, as all fields not bound by the path end in
// the body. This makes this option more rarely used in practice when
// defining REST APIs. The common usage of `*` is in custom methods
// which don't use the URL at all for transferring data.
//
// It is possible to define multiple HTTP methods for one RPC by using
// the `additional_bindings` option. Example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get: "/v1/messages/{message_id}"
// additional_bindings {
// get: "/v1/users/{user_id}/messages/{message_id}"
// }
// };
// }
// }
// message GetMessageRequest {
// string message_id = 1;
// string user_id = 2;
// }
//
// This enables the following two alternative HTTP JSON to RPC mappings:
//
// HTTP | gRPC
// -----|-----
// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id:
// "123456")`
//
// ## Rules for HTTP mapping
//
// 1. Leaf request fields (recursive expansion nested messages in the request
// message) are classified into three categories:
// - Fields referred by the path template. They are passed via the URL path.
// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP
// request body.
// - All other fields are passed via the URL query parameters, and the
// parameter name is the field path in the request message. A repeated
// field can be represented as multiple query parameters under the same
// name.
// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields
// are passed via URL path and HTTP request body.
// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all
// fields are passed via URL path and URL query parameters.
//
// ### Path template syntax
//
// Template = "/" Segments [ Verb ] ;
// Segments = Segment { "/" Segment } ;
// Segment = "*" | "**" | LITERAL | Variable ;
// Variable = "{" FieldPath [ "=" Segments ] "}" ;
// FieldPath = IDENT { "." IDENT } ;
// Verb = ":" LITERAL ;
//
// The syntax `*` matches a single URL path segment. The syntax `**` matches
// zero or more URL path segments, which must be the last part of the URL path
// except the `Verb`.
//
// The syntax `Variable` matches part of the URL path as specified by its
// template. A variable template must not contain other variables. If a variable
// matches a single path segment, its template may be omitted, e.g. `{var}`
// is equivalent to `{var=*}`.
//
// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL`
// contains any reserved character, such characters should be percent-encoded
// before the matching.
//
// If a variable contains exactly one path segment, such as `"{var}"` or
// `"{var=*}"`, when such a variable is expanded into a URL path on the client
// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The
// server side does the reverse decoding. Such variables show up in the
// [Discovery
// Document](https://developers.google.com/discovery/v1/reference/apis) as
// `{var}`.
//
// If a variable contains multiple path segments, such as `"{var=foo/*}"`
// or `"{var=**}"`, when such a variable is expanded into a URL path on the
// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded.
// The server side does the reverse decoding, except "%2F" and "%2f" are left
// unchanged. Such variables show up in the
// [Discovery
// Document](https://developers.google.com/discovery/v1/reference/apis) as
// `{+var}`.
//
// ## Using gRPC API Service Configuration
//
// gRPC API Service Configuration (service config) is a configuration language
// for configuring a gRPC service to become a user-facing product. The
// service config is simply the YAML representation of the `google.api.Service`
// proto message.
//
// As an alternative to annotating your proto file, you can configure gRPC
// transcoding in your service config YAML files. You do this by specifying a
// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same
// effect as the proto annotation. This can be particularly useful if you
// have a proto that is reused in multiple services. Note that any transcoding
// specified in the service config will override any matching transcoding
// configuration in the proto.
//
// Example:
//
// http:
// rules:
// # Selects a gRPC method and applies HttpRule to it.
// - selector: example.v1.Messaging.GetMessage
// get: /v1/messages/{message_id}/{sub.subfield}
//
// ## Special notes
//
// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the
// proto to JSON conversion must follow the [proto3
// specification](https://developers.google.com/protocol-buffers/docs/proto3#json).
//
// While the single segment variable follows the semantics of
// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String
// Expansion, the multi segment variable **does not** follow RFC 6570 Section
// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion
// does not expand special characters like `?` and `#`, which would lead
// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding
// for multi segment variables.
//
// The path variables **must not** refer to any repeated or mapped field,
// because client libraries are not capable of handling such variable expansion.
//
// The path variables **must not** capture the leading "/" character. The reason
// is that the most common use case "{var}" does not capture the leading "/"
// character. For consistency, all path variables must share the same behavior.
//
// Repeated message fields must not be mapped to URL query parameters, because
// no client library can support such complicated mapping.
//
// If an API needs to use a JSON array for request or response body, it can map
// the request or response body to a repeated field. However, some gRPC
// Transcoding implementations may not support this feature.
message HttpRule {
// Selects a method to which this rule applies.
//
// Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
string selector = 1;
// Determines the URL pattern is matched by this rules. This pattern can be
// used with any of the {get|put|post|delete|patch} methods. A custom method
// can be defined using the 'custom' field.
oneof pattern {
// Maps to HTTP GET. Used for listing and getting information about
// resources.
string get = 2;
// Maps to HTTP PUT. Used for replacing a resource.
string put = 3;
// Maps to HTTP POST. Used for creating a resource or performing an action.
string post = 4;
// Maps to HTTP DELETE. Used for deleting a resource.
string delete = 5;
// Maps to HTTP PATCH. Used for updating a resource.
string patch = 6;
// The custom pattern is used for specifying an HTTP method that is not
// included in the `pattern` field, such as HEAD, or "*" to leave the
// HTTP method unspecified for this rule. The wild-card rule is useful
// for services that provide content to Web (HTML) clients.
CustomHttpPattern custom = 8;
}
// The name of the request field whose value is mapped to the HTTP request
// body, or `*` for mapping all request fields not captured by the path
// pattern to the HTTP body, or omitted for not having any HTTP request body.
//
// NOTE: the referred field must be present at the top-level of the request
// message type.
string body = 7;
// Optional. The name of the response field whose value is mapped to the HTTP
// response body. When omitted, the entire response message will be used
// as the HTTP response body.
//
// NOTE: The referred field must be present at the top-level of the response
// message type.
string response_body = 12;
// Additional HTTP bindings for the selector. Nested bindings must
// not contain an `additional_bindings` field themselves (that is,
// the nesting may only be one level deep).
repeated HttpRule additional_bindings = 11;
}
// A custom pattern is used for defining custom HTTP verb.
message CustomHttpPattern {
// The name of this custom HTTP verb.
string kind = 1;
// The path matched by this custom verb.
string path = 2;
}
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.1</Version> <Version>0.0.4</Version>
<PackageId>Foursat.FrontOffice.BFF.ClubMembership.Protobuf</PackageId> <PackageId>Foursat.FrontOffice.BFF.ClubMembership.Protobuf</PackageId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild> <GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
@@ -22,4 +22,15 @@
<ItemGroup> <ItemGroup>
<Protobuf Include="Protos\clubmembership.proto" ProtoRoot="Protos\" GrpcServices="Both" /> <Protobuf Include="Protos\clubmembership.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup> </ItemGroup>
<Target Name="PushToFourSat" AfterTargets="Pack">
<PropertyGroup>
<NugetPackagePath>$(PackageOutputPath)$(PackageId).$(Version).nupkg</NugetPackagePath>
<PushCommand>
dotnet nuget push **/*.nupkg --source https://git.afrino.co/api/packages/FourSat/nuget/index.json --api-key 061a5cb15517c6da39c16cfce8556c55ae104d0d --skip-duplicate
</PushCommand>
</PropertyGroup>
<Exec Command="$(PushCommand)" />
</Target>
</Project> </Project>
@@ -26,6 +26,22 @@ service ClubMembershipContract
body: "*" body: "*"
}; };
}; };
// ارسال OTP برای امضای قرارداد باشگاه مشتریان
rpc RequestClubContractOtp(RequestClubContractOtpRequest) returns (RequestClubContractOtpResponse){
option (google.api.http) = {
post: "/ClubMembership/RequestContractOtp"
body: "*"
};
};
// امضای قرارداد باشگاه مشتریان و فعال‌سازی
rpc AcceptClubMembershipContract(AcceptClubMembershipContractRequest) returns (AcceptClubMembershipContractResponse){
option (google.api.http) = {
post: "/ClubMembership/AcceptContract"
body: "*"
};
};
} }
// ============ GetMyClubMembership ============ // ============ GetMyClubMembership ============
@@ -59,3 +75,35 @@ message ActivateMyClubMembershipResponse
google.protobuf.Timestamp expiration_date = 4; google.protobuf.Timestamp expiration_date = 4;
int64 amount_paid = 5; int64 amount_paid = 5;
} }
// ============ RequestClubContractOtp ============
// درخواست ارسال کد OTP برای امضای قرارداد باشگاه مشتریان
message RequestClubContractOtpRequest
{
string sign_guid = 1; // شناسه یکتای امضا (GUID)
}
message RequestClubContractOtpResponse
{
bool success = 1;
string message = 2;
int32 remaining_attempts = 3;
int32 remaining_seconds = 4;
}
// ============ AcceptClubMembershipContract ============
// امضای قرارداد باشگاه مشتریان
message AcceptClubMembershipContractRequest
{
string otp_code = 1; // کد OTP دریافتی
string sign_guid = 2; // شناسه یکتای امضا
string contract_html = 3; // محتوای HTML قرارداد
}
message AcceptClubMembershipContractResponse
{
bool success = 1;
string message = 2;
int64 contract_id = 3;
string token = 4; // توکن جدید با claims به‌روز شده
}
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.1</Version> <Version>0.0.6</Version>
<PackageId>Foursat.FrontOffice.BFF.Commission.Protobuf</PackageId> <PackageId>Foursat.FrontOffice.BFF.Commission.Protobuf</PackageId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild> <GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
@@ -22,4 +22,15 @@
<ItemGroup> <ItemGroup>
<Protobuf Include="Protos\commission.proto" ProtoRoot="Protos\" GrpcServices="Both" /> <Protobuf Include="Protos\commission.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup> </ItemGroup>
<Target Name="PushToFourSat" AfterTargets="Pack">
<PropertyGroup>
<NugetPackagePath>$(PackageOutputPath)$(PackageId).$(Version).nupkg</NugetPackagePath>
<PushCommand>
dotnet nuget push **/*.nupkg --source https://git.afrino.co/api/packages/FourSat/nuget/index.json --api-key 061a5cb15517c6da39c16cfce8556c55ae104d0d --skip-duplicate
</PushCommand>
</PropertyGroup>
<Exec Command="$(PushCommand)" />
</Target>
</Project> </Project>
@@ -25,6 +25,13 @@ service CommissionContract
get: "/Commission/MyWeeklyBalances" get: "/Commission/MyWeeklyBalances"
}; };
}; };
// دریافت لیست هفته‌ها برای dropdown
rpc GetWeekDefinitions(GetWeekDefinitionsRequest) returns (GetWeekDefinitionsResponse){
option (google.api.http) = {
get: "/Commission/WeekDefinitions"
};
};
} }
// ============ MetaData ============ // ============ MetaData ============
@@ -38,7 +45,7 @@ message GetMyCommissionPayoutsRequest
{ {
int32 page_number = 1; int32 page_number = 1;
int32 page_size = 2; int32 page_size = 2;
google.protobuf.StringValue week_number = 3; google.protobuf.Int64Value week_definition_id = 3;
google.protobuf.Int32Value status = 4; // 0=Pending, 1=Calculated, 2=Paid, 3=Withdrawn google.protobuf.Int32Value status = 4; // 0=Pending, 1=Calculated, 2=Paid, 3=Withdrawn
} }
@@ -51,15 +58,14 @@ message GetMyCommissionPayoutsResponse
message CommissionPayoutModel message CommissionPayoutModel
{ {
int64 id = 1; int64 id = 1;
string week_number = 2; int64 week_definition_id = 2;
string week_label = 3; string week_display_name = 3;
int32 balances_earned = 4; int32 balances_earned = 4;
int64 total_amount = 5; int64 total_amount = 5;
string amount_formatted = 6; string amount_formatted = 6;
string status = 7; int32 status = 7; // 0=Pending, 1=Paid, 2=WithdrawRequested, 3=Withdrawn, 4=PaymentFailed, 5=Cancelled
string status_badge_color = 8; google.protobuf.Timestamp calculated_date = 8;
google.protobuf.Timestamp calculated_date = 9; string date_persian = 9;
string date_persian = 10;
} }
// ============ GetMyWeeklyBalances ============ // ============ GetMyWeeklyBalances ============
@@ -67,7 +73,7 @@ message GetMyWeeklyBalancesRequest
{ {
int32 page_number = 1; int32 page_number = 1;
int32 page_size = 2; int32 page_size = 2;
google.protobuf.StringValue week_number = 3; google.protobuf.Int64Value week_definition_id = 3;
bool only_active = 4; bool only_active = 4;
} }
@@ -83,12 +89,55 @@ message GetMyWeeklyBalancesResponse
message WeeklyBalanceModel message WeeklyBalanceModel
{ {
int64 id = 1; int64 id = 1;
string week_number = 2; int64 week_definition_id = 2;
int32 left_leg_balances = 3; string week_display_name = 3;
int32 right_leg_balances = 4; int32 left_leg_balances = 4; // مجموع پای چپ (NewMembers + Carryover)
int32 total_balances = 5; int32 right_leg_balances = 5; // مجموع پای راست (NewMembers + Carryover)
int64 weekly_pool_contribution = 6; int32 total_balances = 6; // تعداد تعادل = MIN(چپ, راست)
bool is_expired = 7; int64 weekly_pool_contribution = 7;
google.protobuf.Timestamp calculated_at = 8; bool is_expired = 8;
string date_persian = 9; google.protobuf.Timestamp calculated_at = 9;
string date_persian = 10;
int32 left_leg_carryover = 11; // انتقالی پای چپ از هفته قبل
int32 right_leg_carryover = 12; // انتقالی پای راست از هفته قبل
int32 left_leg_new_members = 13; // اعضای جدید پای چپ این هفته
int32 right_leg_new_members = 14; // اعضای جدید پای راست این هفته
}
// ============ GetWeekDefinitions ============
message GetWeekDefinitionsRequest
{
//جستجوی متنی روی DisplayName
google.protobuf.StringValue search_text = 1;
//شماره صفحه
int32 page_number = 2;
//تعداد در صفحه
int32 page_size = 3;
//سال میلادی
google.protobuf.Int32Value gregorian_year = 4;
//سال شمسی
google.protobuf.Int32Value persian_year = 5;
//فقط هفته‌های فعال
google.protobuf.BoolValue is_active = 6;
}
message GetWeekDefinitionsResponse
{
repeated WeekDefinitionModel data = 1;
int32 total_count = 2;
}
message WeekDefinitionModel
{
int64 id = 1;
int32 week_order = 2;
string display_name = 3;
google.protobuf.Timestamp start_date = 4;
google.protobuf.Timestamp end_date = 5;
int32 gregorian_year = 6;
int32 persian_year = 7;
bool is_active = 8;
bool is_current_week = 9;
string start_date_persian = 10;
string end_date_persian = 11;
} }
@@ -0,0 +1,13 @@
using FluentValidation;
using System.Reflection;
namespace Microsoft.Extensions.DependencyInjection;
public static class ConfigureServices
{
public static IServiceCollection AddConfigurationProtobufServices(this IServiceCollection services)
{
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
return services;
}
}
@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.4</Version>
<DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<PackageId>Foursat.FrontOffice.BFF.Configuration.Protobuf</PackageId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
<PackageReference Include="Grpc.Core.Api" Version="2.71.0" />
<PackageReference Include="Grpc.Tools" Version="2.76.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.0" />
<PackageReference Include="Google.Api.CommonProtos" Version="2.17.0" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\configuration.proto" ProtoRoot="Protos\" GrpcServices="Both" />
<Protobuf Include="Protos\appversion.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup>
<Target Name="PushToFourSat" AfterTargets="Pack">
<PropertyGroup>
<NugetPackagePath>$(PackageOutputPath)$(PackageId).$(Version).nupkg</NugetPackagePath>
<PushCommand>
dotnet nuget push **/*.nupkg --source https://git.afrino.co/api/packages/FourSat/nuget/index.json --api-key 061a5cb15517c6da39c16cfce8556c55ae104d0d --skip-duplicate
</PushCommand>
</PropertyGroup>
<Exec Command="$(PushCommand)" />
</Target>
</Project>
@@ -0,0 +1,42 @@
syntax = "proto3";
package appversion;
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option csharp_namespace = "FrontOffice.BFF.Configuration.Protobuf.Protos.AppVersion";
// Service for tracking app versions and cache invalidation
service AppVersionContract
{
// Get current version for an app - called by frontends to check if cache clear is needed
rpc GetAppVersion(GetAppVersionRequest) returns (GetAppVersionResponse){
option (google.api.http) = {
get: "/AppVersion/Get"
};
};
}
// Request to get current version for specific app
message GetAppVersionRequest
{
string app_name = 1; // e.g., "FrontOffice", "BackOffice", "Mobile"
google.protobuf.StringValue current_client_version = 2; // Current version on client for comparison
}
// Response with version info
message GetAppVersionResponse
{
bool found = 1; // Whether the app was found
string app_name = 2;
string current_version = 3;
string min_required_version = 4;
bool requires_full_cache_clear = 5;
bool requires_update = 6; // True if client version < min_required_version
google.protobuf.StringValue update_message = 7;
google.protobuf.StringValue release_notes = 8;
google.protobuf.Timestamp last_updated = 9;
}
@@ -0,0 +1,52 @@
syntax = "proto3";
package configuration;
import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
option csharp_namespace = "FrontOffice.BFF.Configuration.Protobuf.Protos.Configuration";
service ConfigurationContract
{
// دریافت تنظیمات باشگاه مشتریان
rpc GetClubConfiguration(google.protobuf.Empty) returns (GetClubConfigurationResponse){
option (google.api.http) = {
get: "/Configuration/Club"
};
};
// دریافت لیست فیچرهای باشگاه مشتریان
rpc GetClubFeatures(google.protobuf.Empty) returns (GetClubFeaturesResponse){
option (google.api.http) = {
get: "/Configuration/ClubFeatures"
};
};
}
// پاسخ تنظیمات باشگاه
message GetClubConfigurationResponse
{
int64 activation_fee = 1; // هزینه فعال‌سازی (ریال)
int64 membership_gift_value = 2; // مبلغ هدیه عضویت (ریال)
}
// پاسخ فیچرهای باشگاه
message GetClubFeaturesResponse
{
repeated ClubFeatureModel features = 1;
}
message ClubFeatureModel
{
int64 id = 1;
string title = 2;
string description = 3;
bool is_enabled = 4; // آیا فعال است (از UserClubFeature)
int32 display_order = 5; // ترتیب نمایش
google.protobuf.Timestamp granted_at = 6; // تاریخ فعال‌سازی
google.protobuf.Timestamp created_at = 7; // تاریخ ایجاد
string notes = 8; // یادداشت (برای نمایش در مدال جزئیات)
}
@@ -0,0 +1,31 @@
// Copyright (c) 2015, Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package google.api;
import "google/api/http.proto";
import "google/protobuf/descriptor.proto";
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "AnnotationsProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
extend google.protobuf.MethodOptions {
// See `HttpRule`.
HttpRule http = 72295728;
}
@@ -0,0 +1,377 @@
// Copyright 2019 Google LLC.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
syntax = "proto3";
package google.api;
option cc_enable_arenas = true;
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
option java_multiple_files = true;
option java_outer_classname = "HttpProto";
option java_package = "com.google.api";
option objc_class_prefix = "GAPI";
// Defines the HTTP configuration for an API service. It contains a list of
// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
// to one or more HTTP REST API methods.
message Http {
// A list of HTTP configuration rules that apply to individual API methods.
//
// **NOTE:** All service configuration rules follow "last one wins" order.
repeated HttpRule rules = 1;
// When set to true, URL path parameters will be fully URI-decoded except in
// cases of single segment matches in reserved expansion, where "%2F" will be
// left encoded.
//
// The default behavior is to not decode RFC 6570 reserved characters in multi
// segment matches.
bool fully_decode_reserved_expansion = 2;
}
// # gRPC Transcoding
//
// gRPC Transcoding is a feature for mapping between a gRPC method and one or
// more HTTP REST endpoints. It allows developers to build a single API service
// that supports both gRPC APIs and REST APIs. Many systems, including [Google
// APIs](https://github.com/googleapis/googleapis),
// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC
// Gateway](https://github.com/grpc-ecosystem/grpc-gateway),
// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature
// and use it for large scale production services.
//
// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies
// how different portions of the gRPC request message are mapped to the URL
// path, URL query parameters, and HTTP request body. It also controls how the
// gRPC response message is mapped to the HTTP response body. `HttpRule` is
// typically specified as an `google.api.http` annotation on the gRPC method.
//
// Each mapping specifies a URL path template and an HTTP method. The path
// template may refer to one or more fields in the gRPC request message, as long
// as each field is a non-repeated field with a primitive (non-message) type.
// The path template controls how fields of the request message are mapped to
// the URL path.
//
// Example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get: "/v1/{name=messages/*}"
// };
// }
// }
// message GetMessageRequest {
// string name = 1; // Mapped to URL path.
// }
// message Message {
// string text = 1; // The resource content.
// }
//
// This enables an HTTP REST to gRPC mapping as below:
//
// HTTP | gRPC
// -----|-----
// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")`
//
// Any fields in the request message which are not bound by the path template
// automatically become HTTP query parameters if there is no HTTP request body.
// For example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get:"/v1/messages/{message_id}"
// };
// }
// }
// message GetMessageRequest {
// message SubMessage {
// string subfield = 1;
// }
// string message_id = 1; // Mapped to URL path.
// int64 revision = 2; // Mapped to URL query parameter `revision`.
// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`.
// }
//
// This enables a HTTP JSON to RPC mapping as below:
//
// HTTP | gRPC
// -----|-----
// `GET /v1/messages/123456?revision=2&sub.subfield=foo` |
// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield:
// "foo"))`
//
// Note that fields which are mapped to URL query parameters must have a
// primitive type or a repeated primitive type or a non-repeated message type.
// In the case of a repeated type, the parameter can be repeated in the URL
// as `...?param=A&param=B`. In the case of a message type, each field of the
// message is mapped to a separate parameter, such as
// `...?foo.a=A&foo.b=B&foo.c=C`.
//
// For HTTP methods that allow a request body, the `body` field
// specifies the mapping. Consider a REST update method on the
// message resource collection:
//
// service Messaging {
// rpc UpdateMessage(UpdateMessageRequest) returns (Message) {
// option (google.api.http) = {
// patch: "/v1/messages/{message_id}"
// body: "message"
// };
// }
// }
// message UpdateMessageRequest {
// string message_id = 1; // mapped to the URL
// Message message = 2; // mapped to the body
// }
//
// The following HTTP JSON to RPC mapping is enabled, where the
// representation of the JSON in the request body is determined by
// protos JSON encoding:
//
// HTTP | gRPC
// -----|-----
// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id:
// "123456" message { text: "Hi!" })`
//
// The special name `*` can be used in the body mapping to define that
// every field not bound by the path template should be mapped to the
// request body. This enables the following alternative definition of
// the update method:
//
// service Messaging {
// rpc UpdateMessage(Message) returns (Message) {
// option (google.api.http) = {
// patch: "/v1/messages/{message_id}"
// body: "*"
// };
// }
// }
// message Message {
// string message_id = 1;
// string text = 2;
// }
//
//
// The following HTTP JSON to RPC mapping is enabled:
//
// HTTP | gRPC
// -----|-----
// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id:
// "123456" text: "Hi!")`
//
// Note that when using `*` in the body mapping, it is not possible to
// have HTTP parameters, as all fields not bound by the path end in
// the body. This makes this option more rarely used in practice when
// defining REST APIs. The common usage of `*` is in custom methods
// which don't use the URL at all for transferring data.
//
// It is possible to define multiple HTTP methods for one RPC by using
// the `additional_bindings` option. Example:
//
// service Messaging {
// rpc GetMessage(GetMessageRequest) returns (Message) {
// option (google.api.http) = {
// get: "/v1/messages/{message_id}"
// additional_bindings {
// get: "/v1/users/{user_id}/messages/{message_id}"
// }
// };
// }
// }
// message GetMessageRequest {
// string message_id = 1;
// string user_id = 2;
// }
//
// This enables the following two alternative HTTP JSON to RPC mappings:
//
// HTTP | gRPC
// -----|-----
// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")`
// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id:
// "123456")`
//
// ## Rules for HTTP mapping
//
// 1. Leaf request fields (recursive expansion nested messages in the request
// message) are classified into three categories:
// - Fields referred by the path template. They are passed via the URL path.
// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP
// request body.
// - All other fields are passed via the URL query parameters, and the
// parameter name is the field path in the request message. A repeated
// field can be represented as multiple query parameters under the same
// name.
// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields
// are passed via URL path and HTTP request body.
// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all
// fields are passed via URL path and URL query parameters.
//
// ### Path template syntax
//
// Template = "/" Segments [ Verb ] ;
// Segments = Segment { "/" Segment } ;
// Segment = "*" | "**" | LITERAL | Variable ;
// Variable = "{" FieldPath [ "=" Segments ] "}" ;
// FieldPath = IDENT { "." IDENT } ;
// Verb = ":" LITERAL ;
//
// The syntax `*` matches a single URL path segment. The syntax `**` matches
// zero or more URL path segments, which must be the last part of the URL path
// except the `Verb`.
//
// The syntax `Variable` matches part of the URL path as specified by its
// template. A variable template must not contain other variables. If a variable
// matches a single path segment, its template may be omitted, e.g. `{var}`
// is equivalent to `{var=*}`.
//
// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL`
// contains any reserved character, such characters should be percent-encoded
// before the matching.
//
// If a variable contains exactly one path segment, such as `"{var}"` or
// `"{var=*}"`, when such a variable is expanded into a URL path on the client
// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The
// server side does the reverse decoding. Such variables show up in the
// [Discovery
// Document](https://developers.google.com/discovery/v1/reference/apis) as
// `{var}`.
//
// If a variable contains multiple path segments, such as `"{var=foo/*}"`
// or `"{var=**}"`, when such a variable is expanded into a URL path on the
// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded.
// The server side does the reverse decoding, except "%2F" and "%2f" are left
// unchanged. Such variables show up in the
// [Discovery
// Document](https://developers.google.com/discovery/v1/reference/apis) as
// `{+var}`.
//
// ## Using gRPC API Service Configuration
//
// gRPC API Service Configuration (service config) is a configuration language
// for configuring a gRPC service to become a user-facing product. The
// service config is simply the YAML representation of the `google.api.Service`
// proto message.
//
// As an alternative to annotating your proto file, you can configure gRPC
// transcoding in your service config YAML files. You do this by specifying a
// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same
// effect as the proto annotation. This can be particularly useful if you
// have a proto that is reused in multiple services. Note that any transcoding
// specified in the service config will override any matching transcoding
// configuration in the proto.
//
// Example:
//
// http:
// rules:
// # Selects a gRPC method and applies HttpRule to it.
// - selector: example.v1.Messaging.GetMessage
// get: /v1/messages/{message_id}/{sub.subfield}
//
// ## Special notes
//
// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the
// proto to JSON conversion must follow the [proto3
// specification](https://developers.google.com/protocol-buffers/docs/proto3#json).
//
// While the single segment variable follows the semantics of
// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String
// Expansion, the multi segment variable **does not** follow RFC 6570 Section
// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion
// does not expand special characters like `?` and `#`, which would lead
// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding
// for multi segment variables.
//
// The path variables **must not** refer to any repeated or mapped field,
// because client libraries are not capable of handling such variable expansion.
//
// The path variables **must not** capture the leading "/" character. The reason
// is that the most common use case "{var}" does not capture the leading "/"
// character. For consistency, all path variables must share the same behavior.
//
// Repeated message fields must not be mapped to URL query parameters, because
// no client library can support such complicated mapping.
//
// If an API needs to use a JSON array for request or response body, it can map
// the request or response body to a repeated field. However, some gRPC
// Transcoding implementations may not support this feature.
message HttpRule {
// Selects a method to which this rule applies.
//
// Refer to [selector][google.api.DocumentationRule.selector] for syntax details.
string selector = 1;
// Determines the URL pattern is matched by this rules. This pattern can be
// used with any of the {get|put|post|delete|patch} methods. A custom method
// can be defined using the 'custom' field.
oneof pattern {
// Maps to HTTP GET. Used for listing and getting information about
// resources.
string get = 2;
// Maps to HTTP PUT. Used for replacing a resource.
string put = 3;
// Maps to HTTP POST. Used for creating a resource or performing an action.
string post = 4;
// Maps to HTTP DELETE. Used for deleting a resource.
string delete = 5;
// Maps to HTTP PATCH. Used for updating a resource.
string patch = 6;
// The custom pattern is used for specifying an HTTP method that is not
// included in the `pattern` field, such as HEAD, or "*" to leave the
// HTTP method unspecified for this rule. The wild-card rule is useful
// for services that provide content to Web (HTML) clients.
CustomHttpPattern custom = 8;
}
// The name of the request field whose value is mapped to the HTTP request
// body, or `*` for mapping all request fields not captured by the path
// pattern to the HTTP body, or omitted for not having any HTTP request body.
//
// NOTE: the referred field must be present at the top-level of the request
// message type.
string body = 7;
// Optional. The name of the response field whose value is mapped to the HTTP
// response body. When omitted, the entire response message will be used
// as the HTTP response body.
//
// NOTE: The referred field must be present at the top-level of the response
// message type.
string response_body = 12;
// Additional HTTP bindings for the selector. Nested bindings must
// not contain an `additional_bindings` field themselves (that is,
// the nesting may only be one level deep).
repeated HttpRule additional_bindings = 11;
}
// A custom pattern is used for defining custom HTTP verb.
message CustomHttpPattern {
// The name of this custom HTTP verb.
string kind = 1;
// The path matched by this custom verb.
string path = 2;
}
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.1</Version> <Version>0.0.3</Version>
<PackageId>Foursat.FrontOffice.BFF.DiscountShop.Protobuf</PackageId> <PackageId>Foursat.FrontOffice.BFF.DiscountShop.Protobuf</PackageId>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild> <GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
@@ -22,4 +22,15 @@
<ItemGroup> <ItemGroup>
<Protobuf Include="Protos\discountshop.proto" ProtoRoot="Protos\" GrpcServices="Both" /> <Protobuf Include="Protos\discountshop.proto" ProtoRoot="Protos\" GrpcServices="Both" />
</ItemGroup> </ItemGroup>
<Target Name="PushToFourSat" AfterTargets="Pack">
<PropertyGroup>
<NugetPackagePath>$(PackageOutputPath)$(PackageId).$(Version).nupkg</NugetPackagePath>
<PushCommand>
dotnet nuget push **/*.nupkg --source https://git.afrino.co/api/packages/FourSat/nuget/index.json --api-key 061a5cb15517c6da39c16cfce8556c55ae104d0d --skip-duplicate
</PushCommand>
</PropertyGroup>
<Exec Command="$(PushCommand)" />
</Target>
</Project> </Project>

Some files were not shown because too many files have changed in this diff Show More