Compare commits

...

79 Commits

Author SHA1 Message Date
masoodafar-web 2609efafa5 fix: increase Docker daemon startup timeout to 3 minutes
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 0s
2026-01-29 17:13:49 +03:30
masoodafar-web 22e1b2b2b4 trigger: test offline CI/CD pipeline
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 9m1s
2026-01-29 07:03:26 +03:30
masoodafar-web 89d304cfbc refactor: use docker-sshpass image (fully offline)
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-01-29 06:49:36 +03:30
masoodafar-web 376db96f5c refactor: simplified single-job pipeline with offline Nexus support
Build and Deploy to Kubernetes / build-and-deploy (push) Has been cancelled
2026-01-29 06:10:26 +03:30
masoodafar-web 48b5aa7efa fix: add --allow-insecure-connections to nuget push
Build and Deploy / publish-packages (push) Failing after 3m8s
Build and Deploy / build (push) Has been skipped
2026-01-29 04:56:41 +03:30
masoodafar-web 864a760953 feat: add publish-packages job to CI/CD workflow
Build and Deploy / publish-packages (push) Failing after 0s
Build and Deploy / build (push) Has been skipped
- Auto-detect and pack *Protobuf*.csproj files
- Push to Nexus with --skip-duplicate
- Build job now depends on publish-packages
2026-01-29 04:45:18 +03:30
masoodafar-web fa23901980 Revert: use only Nexus (no fallback)
Build and Deploy / build (push) Failing after 5s
2026-01-29 03:44:40 +03:30
masoodafar-web 6b7ca9cd62 Add nuget.org as fallback source
Build and Deploy / build (push) Has been cancelled
2026-01-29 03:41:18 +03:30
masoodafar-web a9648e250e Remove afrino NuGet sources - use only Nexus
Build and Deploy / build (push) Failing after 5s
2026-01-29 03:14:40 +03:30
masoodafar-web 3219d07dea chore: تنظیم Nexus برای NuGet و Docker offline
Build and Deploy / build (push) Failing after 5s
- Dockerfile: استفاده از Nexus Docker (194.5.195.53:32082)
- NuGet.config: استفاده از Nexus NuGet با HTTP و allowInsecureConnections
- workflow: حذف kubectl dependency، اضافه کردن 32082 به insecure-registries
- csproj: تغییر push آدرس به Nexus foursat-nuget-hosted
2026-01-29 02:56:26 +03:30
masoodafar-web 24cbb378c9 fix: Update DNS configuration and enhance cached image loading in kub-deploy.yml
Build and Deploy / build (push) Failing after 4s
2026-01-27 23:30:31 +03:30
masoodafar-web 0674324487 fix: Remove proxy environment variables from Docker container configuration
Build and Deploy / build (push) Failing after 22s
2026-01-27 23:21:40 +03:30
masoodafar-web b86e417e58 fix: Update Docker daemon configuration to enhance image caching and warning messages
Build and Deploy / build (push) Failing after 37s
2026-01-27 23:19:03 +03:30
masoodafar-web 8b66bbcf3d fix: Change Docker build option from --pull=never to --pull=false for better image handling
Build and Deploy / build (push) Failing after 35s
2026-01-27 23:16:26 +03:30
masoodafar-web 273ae390c5 fix: Update Docker daemon configuration to improve image caching and reduce concurrent downloads
Build and Deploy / build (push) Failing after 4s
2026-01-27 23:08:46 +03:30
masoodafar-web 828109f44d chore: Comment out dependency installation steps in kub-deploy.yml
Build and Deploy / build (push) Failing after 8m32s
2026-01-27 22:30:53 +03:30
masoodafar-web 710051f0a0 feat: Add mappings for Update and Delete Discount Category commands in DiscountCategoryProfile
Build and Deploy / build (push) Failing after 2m33s
2026-01-27 21:52:32 +03:30
masoodafar-web aca28fce40 feat: Implement DiscountProductProfile for mapping Discount Product commands and queries
Build and Deploy / build (push) Successful in 2m50s
2026-01-08 18:09:30 +03:30
masoodafar-web b39d1cb2e1 update
Build and Deploy / build (push) Successful in 5m34s
2026-01-08 16:32:00 +03:30
masoodafar-web a420c51b47 fix: Add null check for Description field in Create and Update Discount Category command validators
Build and Deploy / build (push) Successful in 2m17s
2026-01-05 22:57:09 +03:30
masoodafar-web de412dd9e5 fix: Handle null values in response categories and map properties correctly in GetDiscountCategoriesQueryHandler
Build and Deploy / build (push) Successful in 1m52s
2026-01-05 02:14:45 +03:30
masoodafar-web f627cbf46a fix: Handle null response metadata and models in GetDiscountProductsQueryHandler
Build and Deploy / build (push) Successful in 2m2s
2026-01-05 02:07:16 +03:30
masoodafar-web 23b7b15a66 fix: Ensure ShortInformation and FullInformation default to empty string in Create and Update Discount Product commands
Build and Deploy / build (push) Successful in 1m59s
2026-01-05 01:33:50 +03:30
masoodafar-web 0307d0655f refactor: Remove NotEmpty validation for ShortInformation and FullInformation in discount product validators
Build and Deploy / build (push) Successful in 1m52s
2026-01-05 01:29:40 +03:30
masoodafar-web a7a1b61a81 feat: Add image and thumbnail file fields to Create and Update Discount Product requests
Build and Deploy / build (push) Successful in 1m59s
2026-01-05 01:23:41 +03:30
masoodafar-web 1dbe81faaa refactor: Remove InitialCount and RemainingCount properties from discount product commands
Build and Deploy / build (push) Successful in 1m53s
2026-01-04 23:51:47 +03:30
masoodafar-web 3aabf10a51 feat: Implement DiscountCategoryProfile for mapping GetDiscountCategories response to protobuf
Build and Deploy / build (push) Successful in 2m16s
2026-01-04 22:38:39 +03:30
masoodafar-web d2521a7615 update
Build and Deploy / build (push) Successful in 2m10s
2026-01-04 21:52:28 +03:30
Admin 7611e8320b fix: Remove domain from insecure-registries, use IP only
Build and Deploy / build (push) Failing after 11s
2026-01-03 15:15:32 +00:00
Admin 7c89638181 fix: Update registry to IP address for CI/CD
Build and Deploy / build (push) Failing after 3m27s
2026-01-03 15:03:26 +00:00
masoodafar-web 68e6fed06f fix: update external registry URL in deployment workflows
Build and Deploy / build (push) Failing after 2m3s
2026-01-03 17:30:23 +03:30
masoodafar-web f027fd0b06 Refactor inventory commands and queries for improved clarity and consistency
Build and Deploy / build (push) Failing after 2m12s
- Updated ProductType enum values to be more descriptive (RegularProduct, DiscountProduct).
- Removed unnecessary Success and Message properties from response DTOs in AddStock and AdjustStock commands.
- Renamed Note property to Reason in AdjustStock and RecordLoss commands, and added ReferenceNumber.
- Changed InventoryItemId to Id in UpdateInventorySettingsCommand for consistency.
- Updated pagination properties in various queries (PageIndex to Page).
- Enhanced GetAllInventoryItemsQuery and GetLowStockItemsQuery to include sorting options.
- Updated InventoryProfile mappings to reflect changes in DTOs and Protobuf definitions.
- Adjusted Protobuf definitions to align with new property names and structures.
2026-01-03 15:38:22 +03:30
masoodafar-web 989f4a799a fix: specify full namespace for GetAllWarehousesRequest in GetAllWarehousesQueryHandler
Build and Deploy / build (push) Successful in 1m45s
2026-01-03 08:19:01 +03:30
masoodafar-web f6f943947c feat: Implement Inventory Management Commands and Queries
Build and Deploy / build (push) Successful in 1m46s
- Added CreateWarehouseCommand and CreateWarehouseCommandHandler for warehouse creation.
- Introduced RecordLossCommand and RecordLossCommandHandler to handle stock loss recording.
- Created UpdateInventorySettingsCommand and its handler for updating inventory settings.
- Implemented GetAllInventoryItemsQuery and its handler to retrieve inventory items with pagination.
- Added GetAllWarehousesQuery and handler for fetching all warehouses.
- Developed GetLowStockItemsQuery and handler to get items below a specified stock threshold.
- Implemented GetStockMovementsQuery and handler for retrieving stock movement records.
- Created mappings for inventory-related requests and responses in InventoryProfile.
- Developed InventoryService to handle gRPC requests for inventory operations.
- Added Protobuf definitions for inventory management services and messages.
2026-01-03 07:37:32 +03:30
masoodafar-web 0c63b3c83c feat: add ImagePath and ImageDocumentId to ManualPaymentModel and update related query handler
Build and Deploy / build (push) Successful in 1m47s
2026-01-02 02:37:10 +03:30
masoodafar-web 36b30e37b8 feat: enhance CreateManualPayment command with image document ID and update protobuf definitions
Build and Deploy / build (push) Successful in 1m50s
2026-01-02 02:13:51 +03:30
masoodafar-web 31df6d843a feat: add FileUploadDto and image file handling to CreateManualPayment command and protobuf definitions
Build and Deploy / build (push) Successful in 2m23s
2026-01-02 01:19:11 +03:30
masoodafar-web 441098a80b feat: add ImagePath property to CreateManualPayment command and update related protobuf definitions
Build and Deploy / build (push) Successful in 2m22s
2026-01-02 00:45:53 +03:30
masoodafar-web d0224d41b2 feat: Implement Discount Sales Report Query and Handler
Build and Deploy / build (push) Successful in 2m33s
- Added GetDiscountSalesReportQuery and GetDiscountSalesReportQueryHandler to fetch discount sales reports.
- Introduced DiscountSalesReportDto, SalesSummaryDto, and PeriodSalesDto for structured report data.

feat: Add Discount Product Image Management

- Created AddDiscountProductImageCommand and AddDiscountProductImageCommandHandler for image uploads.
- Implemented DeleteDiscountProductImageCommand and DeleteDiscountProductImageCommandHandler for image deletion.
- Added ReorderDiscountProductImagesCommand and ReorderDiscountProductImagesCommandHandler for image reordering.
- Developed UpdateDiscountProductImageCommand and UpdateDiscountProductImageCommandHandler for image updates.

feat: Enhance Product Tag Management

- Introduced DeleteProductTagCommand and DeleteProductTagCommandHandler for tag deletion.
- Added UpdateProductTagCommand and UpdateProductTagCommandHandler for updating product tags.
- Implemented GetAllProductTagsQuery and GetAllProductTagsQueryHandler for fetching all product tags.
- Created GetProductTagQuery and GetProductTagQueryHandler for retrieving specific product tags.

feat: Develop Web API Services for Discount and Product Management

- Implemented DiscountCategoryService for managing discount categories.
- Created DiscountOrderService for handling discount orders and related queries.
- Developed DiscountProductService for managing discount products and their images.
- Added DiscountShoppingCartService for managing shopping cart operations.
- Implemented ProductTagService for managing product tags and their associations.
- Created TagService for managing tags and their related operations.
2026-01-01 02:26:51 +03:30
masoodafar-web 721207f6a0 feat: update GetUserWeeklyBalances response structure and mappings
Build and Deploy / build (push) Successful in 1m47s
Build and Deploy to Production / build-and-deploy (push) Successful in 2m22s
2025-12-27 22:10:30 +03:30
masoodafar-web ce7683c736 feat: enhance GetWithdrawalRequests response structure and update related mappings
Build and Deploy / build (push) Failing after 1m11s
2025-12-27 21:52:50 +03:30
masoodafar-web f5e7d0c882 feat: add mapping for ProcessWithdrawalRequest to ProcessWithdrawalCommand
Build and Deploy / build (push) Successful in 1m39s
Build and Deploy to Production / build-and-deploy (push) Successful in 1m45s
2025-12-27 07:23:00 +03:30
masoodafar-web 86d1370535 feat: implement mapping for GetUserPayoutsResponseDto to GetUserCommissionPayoutsResponse
Build and Deploy / build (push) Successful in 1m58s
2025-12-27 06:45:59 +03:30
masoodafar-web 92aebb42b5 feat: add mapping for MediatR.Unit to Google.Protobuf.Empty
Build and Deploy / build (push) Successful in 1m48s
2025-12-27 02:38:02 +03:30
masoodafar-web c7fc30c83e feat: update Foursat.CMSMicroservice.Protobuf package version and add mapping for GetAllWeeklyPoolsResponse
Build and Deploy / build (push) Successful in 1m48s
2025-12-26 09:44:58 +03:30
masoodafar-web 4c5cff1a9a fix: update CMSMSAddress to remove port from URL
Build and Deploy / build (push) Successful in 1m44s
2025-12-26 07:34:21 +03:30
masoodafar-web 6bc45e4486 feat(bff): Add App Version management endpoints
Build and Deploy to Production / build-and-deploy (push) Successful in 1m47s
- Add appversion.proto with GetAll, Get, Update RPCs
- Add GetAllAppVersionsQuery and handler
- Add UpdateAppVersionCommand and handler
- Add AppVersionService gRPC service
- Add AppVersionProfile for Mapster mappings
- Update IApplicationContractContext with AppVersions client
- Bump Configuration.Protobuf to 1.0.20
2025-12-26 06:07:24 +03:30
masoodafar-web 8f34f0f650 fix: update CMSMSAddress to use secure URL
Build and Deploy to Production / build-and-deploy (push) Successful in 2m17s
2025-12-25 02:22:45 +03:30
masoodafar-web 554d1a65f2 feat: add production appsettings configuration for JWT and Seq
Build and Deploy to Production / build-and-deploy (push) Successful in 2m31s
2025-12-25 02:07:19 +03:30
admin f695290577 fix: correct dockerfile path and registry
Build and Deploy to Production / build-and-deploy (push) Successful in 1m45s
2025-12-23 22:32:25 +00:00
admin 13352879b9 fix: correct dockerfile path and registry
Build and Deploy to Production / build-and-deploy (push) Has been cancelled
2025-12-23 22:32:19 +00:00
admin 464b3e0c1d ci: re-trigger production build
Build and Deploy to Production / build-and-deploy (push) Failing after 17s
2025-12-23 22:31:03 +00:00
admin e627220c98 feat: add production deployment workflow
Build and Deploy to Production / build-and-deploy (push) Failing after 12s
2025-12-23 22:19:23 +00:00
admin 5f7f2a3355 fix: update appsettings for production K8s 2025-12-23 22:16:11 +00:00
admin b8b8a72e57 fix: update appsettings for production K8s 2025-12-23 22:15:51 +00:00
admin b223e37782 fix: update appsettings for production K8s 2025-12-23 22:15:31 +00:00
masoodafar-web ea3577893b fix: update Seq ServerUrl to use HTTPS 2025-12-20 05:19:59 +03:30
masoodafar-web 480d87cec6 fix: update CMSMSAddress and Seq ServerUrl to use secure URLs 2025-12-20 05:18:06 +03:30
masoodafar-web b24191d59f fix: update deployment branch to stage-new in workflow configuration 2025-12-20 05:09:47 +03:30
masoodafar-web f234af49e0 feat: update protobuf versions and enhance club membership and commission queries
Build and Deploy / build (push) Successful in 1m41s
2025-12-20 03:16:36 +03:30
masoodafar-web 6b72e88780 feat: add club activation fields to network membership
Build and Deploy / build (push) Successful in 2m14s
2025-12-14 04:15:22 +03:30
masoodafar-web 4cffed9ce8 fix: update payout mapping and protobuf version
Build and Deploy / build (push) Successful in 2m4s
2025-12-14 02:06:17 +03:30
masoodafar-web c043a640b0 fix: update default Created date to DateTime.Now in payout mapping
Build and Deploy / build (push) Successful in 2m22s
2025-12-14 01:37:35 +03:30
masoodafar-web de0daa5f61 fix: update commission payout mapping and handle null Created date
Build and Deploy / build (push) Successful in 2m30s
2025-12-14 01:28:46 +03:30
masoodafar-web c6abee2650 feat: add manual membership payment and update commission queries
Build and Deploy / build (push) Successful in 2m29s
2025-12-12 10:23:52 +03:30
masoodafar-web 19a717fc87 feat: expand network membership response with additional fields
Build and Deploy / build (push) Successful in 2m12s
2025-12-12 07:52:55 +03:30
masoodafar-web fe56b8f139 feat: add GetAvailableWeeks query and update protobuf imports
Build and Deploy / build (push) Successful in 2m18s
2025-12-12 05:57:32 +03:30
masoodafar-web ce3b5db822 feat: add Mapster profiles and enable DiscountOrder handlers
Build and Deploy / build (push) Successful in 2m14s
2025-12-08 21:10:21 +03:30
masoodafar-web dedc0b809f feat: add HTTP annotations and update protobuf versions
Build and Deploy / build (push) Successful in 2m5s
2025-12-08 05:08:15 +03:30
masoodafar-web ca28678e8f refactor: update protobuf imports and add HTTP annotations 2025-12-08 04:43:03 +03:30
masoodafar-web fd3c17ac23 refactor: update protobuf imports from BFF to CMS
Build and Deploy / build (push) Successful in 2m18s
2025-12-08 01:37:35 +03:30
masoodafar-web 668ba449f1 refactor: update NetworkMembership protobuf import path
Build and Deploy / build (push) Failing after 1m53s
2025-12-08 01:24:37 +03:30
masoud 6392499111 fix: Add port 80 to CMSMSAddress in appsettings.json
Build and Deploy / build (push) Successful in 2m25s
2025-12-07 20:56:17 +00:00
masoodafar-web 5a6dba445b refactor: rename SuperAdmin role to Administrator
Build and Deploy / build (push) Successful in 2m24s
2025-12-07 23:39:22 +03:30
masoud 240214983e fix: update kubeconfig for deployment (127.0.0.1 → 194.5.195.53)
Build and Deploy / build (push) Successful in 2m25s
2025-12-07 19:18:11 +00:00
masoodafar-web 9f64e42029 chore: add trailing space to Serilog debug output
Build and Deploy / build (push) Successful in 2m33s
2025-12-07 22:27:28 +03:30
masoud dbe7bc2b78 chore: test multi-remote push to git.afrino.co
Build and Deploy / build (push) Has been cancelled
2025-12-07 18:52:35 +00:00
masoud d9675ed345 chore: trigger rebuild with new registry domain
Build and Deploy / build (push) Successful in 2m5s
2025-12-07 17:32:23 +00:00
masoud a41b85a4ae chore: migrate from IP to domain (git.foursat.afrino.co)
Build and Deploy / build (push) Has been cancelled
2025-12-07 17:01:56 +00:00
235 changed files with 8777 additions and 1108 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ name: Push nuget and docker image Actions Workflow
on:
push:
branches:
- stage
- stage-new
jobs:
Deploy:
runs-on: windows
+52 -53
View File
@@ -1,4 +1,4 @@
name: Build and Deploy
name: Build and Deploy to Kubernetes
on:
push:
@@ -6,85 +6,84 @@ on:
- kub-stage
env:
REGISTRY: gitea-svc:3000
REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/backoffice-bff
K8S_SERVER: 194.5.195.53
jobs:
build:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: docker:latest
image: 194.5.195.53:32082/docker-sshpass:latest
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:
- name: Install dependencies
run: |
apk add --no-cache git curl
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/
- name: Start Docker daemon with insecure registry
- name: Start Docker daemon
run: |
mkdir -p /etc/docker
cat > /etc/docker/daemon.json << 'DAEMON'
{
"insecure-registries": ["194.5.195.53:30080", "gitea-svc:3000"]
"insecure-registries": ["194.5.195.53:30080", "194.5.195.53:32500", "194.5.195.53:32082"]
}
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
echo "🚀 Starting Docker daemon..."
dockerd &
for i in $(seq 1 30); do
docker info >/dev/null 2>&1 && break || sleep 2
# Wait up to 3 minutes for Docker to be ready
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
docker info
# Final check
if ! docker info >/dev/null 2>&1; then
echo "❌ Docker daemon failed to start after 3 minutes"
exit 1
fi
- name: Checkout code
run: |
git clone --depth 1 --branch kub-stage http://gitea-svc:3000/admin/BackOffice.BFF.git .
git log -1 --format="%H %s"
- name: Publish Protobuf packages
run: |
echo "📦 Publishing Protobuf packages..."
docker run --rm -v $(pwd):/src -w /src \
194.5.195.53:32082/dotnet/sdk:9.0 sh -c '
for proj in $(find . -name "*Protobuf*.csproj" -type f); do
echo "📦 $proj"
dotnet restore "$proj"
dotnet build "$proj" -c Release --no-restore
dotnet pack "$proj" -c Release --no-build -o "$(dirname $proj)/nupkg"
for nupkg in $(dirname $proj)/nupkg/*.nupkg; do
[ -f "$nupkg" ] && dotnet nuget push "$nupkg" \
--source "http://194.5.195.53:32081/repository/foursat-nuget-hosted/index.json" \
--api-key "admin:87zH26nbqT" \
--skip-duplicate --allow-insecure-connections || true
done
done
'
echo "✅ Protobuf packages done!"
- name: Build Docker Image
run: |
cd src
docker build -f BackOffice.BFF.WebApi/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
--build-arg HTTP_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
--build-arg HTTPS_PROXY=http://proxyuser:87zH26nbqT2@46.249.98.211:3128 \
.
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest -f src/BackOffice.BFF.WebApi/Dockerfile .
- 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 }}: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/backoffice-bff || echo "Deployment doesn't exist yet"
# Wait for rollout to complete
kubectl rollout status deployment/backoffice-bff --timeout=5m || echo "Deployment rollout pending"
export SSHPASS="${{ secrets.SERVER_PASSWORD }}"
sshpass -e ssh -o StrictHostKeyChecking=no root@${{ env.K8S_SERVER }} "
kubectl rollout restart deployment/backoffice-bff
kubectl rollout status deployment/backoffice-bff --timeout=180s
"
echo "Deployed!"
+85
View File
@@ -0,0 +1,85 @@
name: Build and Deploy to Production
on:
push:
branches:
- production
env:
REGISTRY: gitea-svc:3000
EXTERNAL_REGISTRY: 194.5.195.53:30080
IMAGE_NAME: admin/backoffice-bff
jobs:
build-and-deploy:
runs-on: ubuntu-latest
container:
image: docker:latest
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,45.149.79.127,194.5.195.53,10.0.0.0/8
steps:
- name: Install dependencies
run: |
apk add --no-cache git curl
# Install kubectl with fixed version
KUBECTL_VERSION="v1.31.0"
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod +x kubectl
mv kubectl /usr/local/bin/
- 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", "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 &
for i in $(seq 1 30); do docker info >/dev/null 2>&1 && break || sleep 2; done
- name: Checkout code
run: |
git clone --depth 1 --branch production http://gitea-svc:3000/admin/BackOffice.BFF.git .
git log -1 --format="%H %s"
- name: Build Docker Image
run: |
cd src
docker build -f BackOffice.BFF.WebApi/Dockerfile \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
-t ${{ env.EXTERNAL_REGISTRY }}/${{ env.IMAGE_NAME }}:prod \
--build-arg HTTP_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
run: |
echo "87zH26nbqT" | 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
echo "87zH26nbqT" | docker login ${{ env.EXTERNAL_REGISTRY }} -u admin --password-stdin
docker push ${{ env.EXTERNAL_REGISTRY }}/${{ env.IMAGE_NAME }}:prod
- name: Deploy to Production
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_PROD }}" | base64 -d > ~/.kube/config
kubectl rollout restart deployment/backoffice-bff || echo "Deployment not found"
kubectl rollout status deployment/backoffice-bff --timeout=5m || echo "Rollout pending"
+606
View File
@@ -0,0 +1,606 @@
# Plan: فعال‌سازی مرحله‌ای Handler های کامنت‌شده
**تاریخ:** 8 دسامبر 2025
**وضعیت:** ✅ کامل شده
**آخرین به‌روزرسانی:** 1 ژانویه 2026
---
## خلاصه اجرایی
در `BackOffice.BFF.Application.csproj` سه دسته handler کامنت شده بودند که همه فعال شدند:
1. **DiscountOrderCQ/** (18 فایل) - ✅ فعال شد
2. **DiscountShoppingCartCQ/** (16 فایل) - ✅ فعال شد و با CMS proto هماهنگ شد
3. **CommissionCQ/Commands/ProcessWithdrawal/** (3 فایل) - ✅ قبلاً فعال شده بود
**همچنین در BackOffice UI (1 ژانویه 2026):**
- فعال‌سازی همه سرویس‌ها در `ConfigureService.cs`
- ثبت gRPC Clients برای DiscountProduct, DiscountCategory, DiscountOrder, Tag, ProductTag, PublicMessage
- ثبت Application Services
---
## تغییرات انجام‌شده (1 ژانویه 2026)
### BackOffice UI - فعال‌سازی فرانت‌اند
#### ConfigureService.cs
- فعال‌سازی using statements برای همه proto clients
- ثبت gRPC Clients در DI container
- ثبت Application Services (IDiscountProductService, IDiscountCategoryService, etc.)
#### صفحات فعال شده:
| Route | صفحه |
|-------|------|
| `/discount-products` | مدیریت محصولات تخفیفی |
| `/discount-categories` | مدیریت دسته‌بندی‌ها |
| `/discount-orders` | مدیریت سفارشات |
| `/tags` | مدیریت تگ‌ها |
| `/public-messages` | پیام‌های عمومی |
---
## تغییرات انجام‌شده (13 ژانویه 2025)
### DiscountShoppingCartCQ - اصلاحات Proto
#### RemoveFromCart
- `CartItemId``ProductId` (مطابق با proto)
#### UpdateCartItemCount
- `CartItemId``ProductId` (مطابق با proto)
#### GetUserCart Response
- حذف `UserId` از response
- `TotalDiscountedPrice``TotalDiscountAmount`
- `TotalSavings` → حذف شد
- اضافه شدن `FinalPrice`
#### CartItemDto
- حذف `Id`
- `DiscountedPrice``DiscountAmount`
- `AddedAt``Created`
- اضافه شدن `FinalPrice`, `ProductRemainingCount`
---
## مرحله 1: ProcessWithdrawal ✅ (قبلاً انجام شده)
### ویژگی بیزینسی
مدیریت درخواست‌های برداشت کمیسیون توسط ادمین (تایید/رد)
### فایل‌های دخیل
```
CommissionCQ/Commands/ProcessWithdrawal/
├── ProcessWithdrawalCommand.cs
├── ProcessWithdrawalCommandHandler.cs
└── ProcessWithdrawalCommandValidator.cs
```
### مشکل فعلی
```csharp
// Handler - خط 25
var grpcRequest = new ProcessWithdrawalRequest
{
PayoutId = request.WithdrawalId,
IsApproved = true, // ⚠️ هاردکد شده!
Reason = request.AdminNote != null
? new StringValue { Value = request.AdminNote }
: null
};
```
### تغییرات مورد نیاز
#### 1. آپدیت Command
```csharp
// ProcessWithdrawalCommand.cs
public record ProcessWithdrawalCommand : IRequest<ProcessWithdrawalResponseDto>
{
public long WithdrawalId { get; init; }
public bool IsApproved { get; init; } // ✅ جدید
public string? Reason { get; init; } // نام‌گذاری مجدد از AdminNote
}
```
#### 2. آپدیت Handler
```csharp
// ProcessWithdrawalCommandHandler.cs
var grpcRequest = new ProcessWithdrawalRequest
{
PayoutId = request.WithdrawalId,
IsApproved = request.IsApproved, // ✅ از command گرفته شود
Reason = !string.IsNullOrEmpty(request.Reason)
? new StringValue { Value = request.Reason }
: null
};
var response = await _context.Commissions.ProcessWithdrawalAsync(
grpcRequest,
cancellationToken);
return new ProcessWithdrawalResponseDto
{
Success = true,
Message = "Withdrawal processed successfully"
};
```
#### 3. Uncomment WebApi Service
```csharp
// CommissionService.cs - خطوط 90-96
public override async Task<Empty> ProcessWithdrawal(
ProcessWithdrawalRequest request,
ServerCallContext context)
{
await _dispatchRequestToCQRS.Handle<
ProcessWithdrawalRequest,
ProcessWithdrawalCommand,
ProcessWithdrawalResponseDto>(request, context);
return new Empty();
}
```
#### 4. حذف از Exclude List
```xml
<!-- BackOffice.BFF.Application.csproj -->
<!-- حذف این خط: -->
<Compile Remove="CommissionCQ/Commands/ProcessWithdrawal/**/*.cs" />
```
### چک‌لیست
- [ ] آپدیت ProcessWithdrawalCommand با فیلد IsApproved
- [ ] آپدیت ProcessWithdrawalCommandHandler - حذف hardcode
- [ ] Uncomment متد در CommissionService.cs
- [ ] حذف از exclude در Application.csproj
- [ ] Build و تست
---
## مرحله 2: DiscountShoppingCartCQ (2-3 ساعت - متوسط 📊)
### ویژگی بیزینسی
مدیریت سبد خرید فروشگاه تخفیف برای مشتریان
### فایل‌های دخیل
```
DiscountShoppingCartCQ/
├── Commands/ (12 فایل)
│ ├── AddToCart/ ✅ تطابق دارد
│ ├── RemoveFromCart/ ✅ تطابق دارد
│ ├── UpdateCartItemCount/ ✅ تطابق دارد
│ └── ClearCart/ ✅ تطابق دارد
└── Queries/ (4 فایل)
├── GetUserCart/ ⚠️ نیاز به Mapster
└── سایر queries...
```
### مشکل فعلی: GetUserCartResponse
#### Handler انتظار دارد:
```csharp
public class GetUserCartResponseDto
{
public long UserId { get; set; } // ❌ در CMS proto نیست
public decimal TotalPrice { get; set; }
public decimal TotalDiscountedPrice { get; set; } // ❌ نام متفاوت
public decimal TotalSavings { get; set; } // ❌ نام متفاوت
public List<CartItemDto> Items { get; set; }
}
public class CartItemDto
{
public long Id { get; set; } // ❌ در CMS proto نیست
public decimal DiscountedPrice { get; set; } // ❌ نام: final_price
public DateTime AddedAt { get; set; } // ❌ نام: created (Timestamp)
}
```
#### CMS Proto دارد:
```protobuf
message GetUserCartResponse {
repeated CartItemDto items = 1;
int64 total_price = 2;
int64 total_discount_amount = 3; // ← TotalSavings
int64 final_price = 4; // ← TotalDiscountedPrice
}
message CartItemDto {
int64 product_id = 1;
string product_title = 2;
string product_image_path = 3;
int64 unit_price = 4;
int32 max_discount_percent = 5;
int32 count = 6;
int64 total_price = 7;
int64 discount_amount = 8;
int64 final_price = 9; // ← DiscountedPrice
int32 product_remaining_count = 10;
google.protobuf.Timestamp created = 11; // ← AddedAt
}
```
### تغییرات مورد نیاز
#### 1. ساخت Mapster Profile
```csharp
// BackOffice.BFF.WebApi/Common/Mappings/DiscountShoppingCartProfile.cs
using Mapster;
using BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
namespace BackOffice.BFF.WebApi.Common.Mappings;
public class DiscountShoppingCartProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// Map GetUserCart Response
config.NewConfig<GetUserCartResponse, GetUserCartResponseDto>()
.MapWith(src => new GetUserCartResponseDto
{
// UserId باید از request گرفته شود (در handler)
TotalPrice = src.TotalPrice,
TotalDiscountedPrice = src.FinalPrice,
TotalSavings = src.TotalDiscountAmount,
Items = src.Items.Select(item => new CartItemDto
{
// Id ندارد - می‌تواند 0 باشد یا از product_id استفاده شود
Id = item.ProductId,
ProductId = item.ProductId,
ProductTitle = item.ProductTitle,
ProductImagePath = item.ProductImagePath,
UnitPrice = item.UnitPrice,
MaxDiscountPercent = item.MaxDiscountPercent,
Count = item.Count,
TotalPrice = item.TotalPrice,
DiscountedPrice = item.FinalPrice,
AddedAt = item.Created.ToDateTime()
}).ToList()
});
}
}
```
#### 2. آپدیت Handler
```csharp
// GetUserCartQueryHandler.cs
public async Task<GetUserCartResponseDto> Handle(
GetUserCartQuery request,
CancellationToken cancellationToken)
{
var grpcRequest = new GetUserCartRequest
{
UserId = request.UserId
};
var response = await _context.DiscountShoppingCarts.GetUserCartAsync(
grpcRequest,
cancellationToken: cancellationToken);
var result = TypeAdapter.Adapt(
response,
response.GetType(),
typeof(GetUserCartResponseDto)) as GetUserCartResponseDto;
// UserId را از request می‌گیریم چون در proto نیست
result.UserId = request.UserId;
return result;
}
```
#### 3. حذف از Exclude List
```xml
<!-- BackOffice.BFF.Application.csproj -->
<!-- حذف این خط: -->
<Compile Remove="DiscountShoppingCartCQ/**/*.cs" />
```
### چک‌لیست
- [ ] ساخت DiscountShoppingCartProfile.cs
- [ ] آپدیت GetUserCartQueryHandler با Mapster
- [ ] تست mapping با داده واقعی
- [ ] حذف از exclude در Application.csproj
- [ ] Build و تست endpoint
### سوال کلیدی ⚠️
**آیا BackOffice.BFF نیاز به مدیریت سبد خرید دارد؟**
- اگر خیر: Document کنیم "Not applicable - FrontOffice only"
- اگر بله: Mapster profile بسازیم
---
## مرحله 3: DiscountOrderCQ (6-8 ساعت - پیچیده 🔴)
### ویژگی بیزینسی
مدیریت سفارش‌های فروشگاه تخفیف توسط ادمین
### فایل‌های دخیل
```
DiscountOrderCQ/
├── Commands/ (10 فایل)
│ ├── PlaceOrder/ ⚠️ Proto مختلف
│ ├── CompletePayment/ ⚠️ Proto مختلف
│ ├── UpdateOrderStatus/ ⚠️ Proto مختلف
│ └── سایر commands...
└── Queries/ (8 فایل)
├── GetOrderById/ ⚠️ Proto مختلف
├── GetAllUserOrders/ ⚠️ Proto مختلف
└── سایر queries...
```
### مشکل اصلی: دو Proto متفاوت
#### تفاوت‌های کلیدی
**1. PlaceOrderRequest**
Handler انتظار دارد:
```csharp
UserId, AddressId, DiscountBalanceAmount, GatewayAmount
```
CMS Proto دارد:
```protobuf
user_id, user_address_id, discount_balance_to_use, notes (StringValue)
```
**2. PlaceOrderResponse**
Handler انتظار دارد:
```csharp
OrderId, TrackingCode, RequiresGatewayPayment, GatewayPayableAmount
```
CMS Proto دارد:
```protobuf
success, message, order_id, gateway_amount, payment_url (StringValue)
```
**3. GetOrderByIdResponse - تفاوت اصلی**
Handler انتظار دارد:
```csharp
public class OrderDto
{
public long Id { get; set; }
public int Status { get; set; } // ← int
public string StatusTitle { get; set; } // ← محاسبه شده
public bool IsPaid { get; set; } // ← bool
public DateTime? PaidAt { get; set; }
public string UserFullName { get; set; }
public string UserMobile { get; set; }
public string DeliveryAddress { get; set; } // ← string
// ...
}
```
CMS Proto دارد:
```protobuf
message OrderDto {
int64 id = 1;
DeliveryStatus delivery_status = 2; // ← enum
bool payment_completed = 3; // ← bool
google.protobuf.Timestamp created = 4;
AddressDto address = 5; // ← object
// StatusTitle ندارد - باید derive شود
// PaidAt ندارد - باید از created استفاده شود
}
enum DeliveryStatus {
DELIVERY_PENDING = 0;
DELIVERY_PROCESSING = 1;
DELIVERY_SHIPPED = 2;
DELIVERY_DELIVERED = 3;
DELIVERY_CANCELLED = 4;
}
```
### تصمیم کلیدی ⚠️
**گزینه A: استفاده از CMS Proto (پیشنهادی)**
- ✅ CMS قبلاً پیاده‌سازی شده
- ✅ ریسک کمتر
- ❌ نیاز به rewrite کردن 18 handler
- ❌ 6-8 ساعت کار
**گزینه B: استفاده از BFF Proto**
- ✅ Handler ها آماده هستند
- ❌ نیاز به آپدیت CMS microservice
- ❌ ریسک بالا
- ❌ تست‌های بیشتر
### تغییرات مورد نیاز (گزینه A)
#### 1. ساخت Mapster Profile جامع
```csharp
// DiscountOrderProfile.cs
public class DiscountOrderProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
// PlaceOrder Command → CMS Request
config.NewConfig<PlaceOrderCommand, CMSPlaceOrderRequest>()
.MapWith(src => new CMSPlaceOrderRequest
{
UserId = src.UserId,
UserAddressId = src.AddressId,
DiscountBalanceToUse = src.DiscountBalanceAmount,
Notes = !string.IsNullOrEmpty(src.Notes)
? new StringValue { Value = src.Notes }
: null
});
// CMS Response → DTO
config.NewConfig<CMSPlaceOrderResponse, PlaceOrderResponseDto>()
.MapWith(src => new PlaceOrderResponseDto
{
OrderId = src.OrderId,
TrackingCode = src.OrderId.ToString(),
RequiresGatewayPayment = src.GatewayAmount > 0,
GatewayPayableAmount = src.GatewayAmount,
PaymentUrl = src.PaymentUrl?.Value
});
// GetOrderById - پیچیده‌ترین mapping
config.NewConfig<CMSOrderDto, OrderDto>()
.MapWith(src => new OrderDto
{
Id = src.Id,
Status = (int)src.DeliveryStatus,
StatusTitle = GetStatusTitle(src.DeliveryStatus),
IsPaid = src.PaymentCompleted,
PaidAt = src.PaymentCompleted
? src.Created.ToDateTime()
: null,
UserFullName = src.UserFullName,
UserMobile = src.UserMobile,
DeliveryAddress = FormatAddress(src.Address),
// ... بقیه فیلدها
});
// GetAllUserOrders
config.NewConfig<CMSGetAllUserOrdersResponse, GetAllUserOrdersResponseDto>()
.MapWith(src => new GetAllUserOrdersResponseDto
{
MetaData = new MetaData
{
PageNumber = src.MetaData.CurrentPage,
PageSize = src.MetaData.PageSize,
TotalPages = src.MetaData.TotalPage,
TotalCount = src.MetaData.TotalCount
},
Orders = src.Orders.Select(o => new OrderSummaryDto
{
Id = o.Id,
Status = (int)o.DeliveryStatus,
StatusTitle = GetStatusTitle(o.DeliveryStatus),
// ...
}).ToList()
});
}
private static string GetStatusTitle(DeliveryStatus status) => status switch
{
DeliveryStatus.DeliveryPending => "در انتظار پردازش",
DeliveryStatus.DeliveryProcessing => "در حال پردازش",
DeliveryStatus.DeliveryShipped => "ارسال شده",
DeliveryStatus.DeliveryDelivered => "تحویل داده شده",
DeliveryStatus.DeliveryCancelled => "لغو شده",
_ => "نامشخص"
};
private static string FormatAddress(AddressDto address)
{
if (address == null) return string.Empty;
return $"{address.Province}, {address.City}, {address.Street}, " +
$"پلاک {address.PlateNumber}, واحد {address.Unit}";
}
}
```
#### 2. بازنویسی Handlers (نمونه)
```csharp
// PlaceOrderCommandHandler.cs
public async Task<PlaceOrderResponseDto> Handle(
PlaceOrderCommand request,
CancellationToken cancellationToken)
{
var grpcRequest = TypeAdapter.Adapt(
request,
request.GetType(),
typeof(CMSPlaceOrderRequest)) as CMSPlaceOrderRequest;
var response = await _context.DiscountOrders.PlaceOrderAsync(
grpcRequest,
cancellationToken: cancellationToken);
return TypeAdapter.Adapt(
response,
response.GetType(),
typeof(PlaceOrderResponseDto)) as PlaceOrderResponseDto;
}
```
#### 3. حذف از Exclude List
```xml
<!-- BackOffice.BFF.Application.csproj -->
<!-- حذف این خط: -->
<Compile Remove="DiscountOrderCQ/**/*.cs" />
```
### چک‌لیست
- [ ] تصمیم‌گیری: CMS proto یا BFF proto؟
- [ ] مقایسه دقیق line-by-line دو proto
- [ ] ساخت DiscountOrderProfile.cs جامع
- [ ] بازنویسی PlaceOrderCommandHandler
- [ ] بازنویسی CompletePaymentCommandHandler
- [ ] بازنویسی UpdateOrderStatusCommandHandler
- [ ] بازنویسی GetOrderByIdQueryHandler
- [ ] بازنویسی GetAllUserOrdersQueryHandler
- [ ] تست کامل flow: Place → Pay → Update → Get
- [ ] حذف از exclude در Application.csproj
- [ ] Build و تست همه endpoints
---
## جدول خلاصه
| Handler Group | تعداد فایل | Proto Source | BFF Proto | Mapster Profile | پیچیدگی | زمان تخمینی | اولویت بیزینسی |
|---------------|-----------|--------------|-----------|-----------------|---------|-------------|----------------|
| ProcessWithdrawal | 3 | BFF.Commission | ✅ | ❌ نیاز نیست | ساده | 30 دقیقه | متوسط ⚠️ |
| DiscountShoppingCart | 16 | CMS | ✅ (متفاوت) | ❌ باید ساخت | متوسط | 2-3 ساعت | متوسط ⚠️ |
| DiscountOrder | 18 | CMS | ✅ (کاملاً متفاوت) | ❌ باید ساخت | پیچیده | 6-8 ساعت | بالا 🔴 |
| **جمع کل** | **37** | - | - | - | - | **8.5-11.5 ساعت** | - |
---
## سوالات کلیدی برای تصمیم‌گیری
### 1. DiscountShoppingCartCQ
**سوال:** آیا BackOffice نیاز به مدیریت سبد خرید دارد؟
- اگر **خیر**: این feature فقط برای FrontOffice است → Document و نگه‌داری exclude
- اگر **بله**: ادمین باید بتواند سبد خرید کاربران را ببیند → Mapster profile بسازیم
### 2. DiscountOrderCQ
**سوال:** کدام proto را استفاده کنیم؟
- **گزینه A (پیشنهادی)**: CMS proto
- Handler ها را rewrite می‌کنیم
- 6-8 ساعت کار
- ریسک کم
- **گزینه B**: BFF proto
- CMS microservice را آپدیت می‌کنیم
- زمان نامشخص
- ریسک بالا
### 3. اولویت اجرا
کدام مرحله اول اجرا شود؟
- **پیشنهاد:** ProcessWithdrawal (سریع‌ترین ROI)
- سپس: بر اساس نیاز بیزینسی
---
## مراحل بعدی
### فوری
1. ✅ تصمیم: DiscountShoppingCart نیاز هست؟
2. ✅ تصمیم: DiscountOrder از کدام proto؟
3. ✅ شروع با ProcessWithdrawal (30 دقیقه)
### کوتاه‌مدت
4. اگر نیاز: DiscountShoppingCart (2-3 ساعت)
5. Planning دقیق DiscountOrder (1 ساعت)
### میان‌مدت
6. پیاده‌سازی DiscountOrder (6-8 ساعت)
7. تست integration کامل
8. Document کردن تغییرات
---
**تاریخ آخرین آپدیت:** 8 دسامبر 2025
**وضعیت:** منتظر تصمیم‌گیری و شروع اجرا
**مسئول:** تیم توسعه BackOffice.BFF
+599
View File
@@ -0,0 +1,599 @@
# گزارش کامل مهاجرت به Mapster و فعال‌سازی Handler ها
> تاریخ تکمیل: December 8, 2025
>
> وضعیت: ✅ **تکمیل شده - 0 خطا**
## خلاصه اجرایی
تمامی Handler های پروژه BackOffice.BFF با موفقیت به Mapster مهاجرت داده شدند و فعال گردیدند. تنها Handler غیرفعال باقیمانده `DiscountShoppingCartCQ` است که یک feature مختص FrontOffice می‌باشد.
### نتایج کلیدی
-**18 فایل** در DiscountOrderCQ اصلاح شد
-**3 فایل** در ProcessWithdrawal اصلاح شد
-**7 Mapster Profile** ایجاد شد
-**0 Error** در Build نهایی
-**تمام BFF Protobuf Contract ها** به درستی پیاده‌سازی شدند
---
## 📋 فهرست Handler های اصلاح شده
### 1. ProcessWithdrawal ✅ (اولویت 1)
**مدت زمان**: 30 دقیقه
**وضعیت**: فعال و آماده
#### تغییرات انجام شده:
1. **ProcessWithdrawalCommand.cs**
- اضافه شدن فیلد: `public bool IsApproved { get; init; }`
2. **ProcessWithdrawalCommandHandler.cs**
- حذف مقدار hardcoded: `IsApproved = true`
- استفاده از فیلد دریافتی: `IsApproved = request.IsApproved`
3. **BackOffice.BFF.Application.csproj**
- حذف exclude: `ProcessWithdrawalCQ/**/*.cs`
4. **WithdrawService.cs** (WebApi)
- فعال‌سازی متد: `ProcessWithdrawalAsync`
**نتیجه**: Handler با موفقیت فعال شد و قابلیت تایید/رد برداشت را دارد.
---
### 2. DiscountShoppingCart 📝 (اولویت 2)
**مدت زمان**: 5 دقیقه
**وضعیت**: مستندسازی شده (FrontOffice-only)
#### تصمیم معماری:
```xml
<!-- DiscountShoppingCart - FrontOffice-only feature, not needed in BackOffice -->
<!-- این feature فقط برای مشتریان FrontOffice است. مدیریت سبد خرید توسط ادمین در آینده اضافه خواهد شد -->
<Compile Remove="DiscountShoppingCartCQ/**/*.cs" />
```
**دلیل**: سبد خرید تخفیفی یک feature مختص پنل کاربری است. BackOffice نیازی به مدیریت سبد خرید ندارد.
---
### 3. DiscountOrderCQ ✅ (اولویت 3)
**مدت زمان**: 120 دقیقه
**وضعیت**: فعال و آماده
#### فایل‌های اصلاح شده (18 فایل):
##### **Mapster Profiles (2 فایل)**
1. **BackOffice.BFF.Application/Common/Mappings/DiscountOrderProfile.cs** (190 خط)
- PlaceOrder: Command → Request, Response → DTO
- CompleteOrderPayment: Command → Request, Response → DTO
- UpdateOrderStatus: Command → Request, Response → DTO
- GetOrderById: Response → DTO (با AddressInfo و OrderItem)
- GetUserOrders: Response → DTO (با MetaData و pagination)
- Helper Methods: GetDeliveryStatusTitle(), ExtractProvince(), ExtractCity()
2. **BackOffice.BFF.WebApi/Common/Mappings/DiscountOrderProfile.cs** (174 خط)
- نقشه‌برداری از BFF Proto به Application DTOs
- مدیریت StringValue و Timestamp conversion
- محاسبات: FinalPrice، RequiresGatewayPayment
##### **Commands (3 فایل)**
3. **PlaceOrderCommand.cs**
- ❌ حذف شد: `public long GatewayAmount { get; init; }`
- ✅ اضافه شد: `public string? Notes { get; init; }`
4. **CompleteOrderPaymentCommand.cs**
- ❌ حذف شد: `public long PaidAmount { get; init; }`
- ✅ اضافه شد: `public bool PaymentSuccess { get; init; }`
- ✅ تغییر Return Type: `IRequest``IRequest<CompleteOrderPaymentResponseDto>`
5. **UpdateOrderStatusCommand.cs**
- ✅ اضافه شد: `public string? TrackingCode { get; init; }`
- ✅ تغییر Return Type: `IRequest``IRequest<UpdateOrderStatusResponseDto>`
##### **Response DTOs (2 فایل جدید)**
6. **CompleteOrderPaymentResponseDto.cs**
```csharp
public class CompleteOrderPaymentResponseDto
{
public bool Success { get; init; }
public string Message { get; init; }
}
```
7. **UpdateOrderStatusResponseDto.cs**
```csharp
public class UpdateOrderStatusResponseDto
{
public bool Success { get; init; }
public string Message { get; init; }
}
```
##### **Query (1 فایل)**
8. **GetOrderByIdQuery.cs**
- ✅ اضافه شد: `public long UserId { get; init; }` (برای authorization check)
##### **Handlers (5 فایل)**
9. **PlaceOrderCommandHandler.cs**
- **قبل**: 30 خط با manual mapping
- **بعد**: 12 خط با TypeAdapter.Adapt
- Import: `BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder`
10. **CompleteOrderPaymentCommandHandler.cs**
- **قبل**: Return `Unit.Value`
- **بعد**: Return `CompleteOrderPaymentResponseDto`
- استفاده از TypeAdapter برای request و response
11. **UpdateOrderStatusCommandHandler.cs**
- **قبل**: Return `Unit.Value`
- **بعد**: Return `UpdateOrderStatusResponseDto`
- Enum conversion: `(DeliveryStatus)request.NewStatus`
12. **GetOrderByIdQueryHandler.cs**
- **قبل**: 59 خط با manual mapping (50+ فیلد)
- **بعد**: 27 خط با single TypeAdapter call
- ✅ رفع bug: File corruption (orphaned code)
- ✅ اضافه شد: `UserId = request.UserId` در grpcRequest
13. **GetUserOrdersQueryHandler.cs**
- **قبل**: 55 خط با manual MetaData/Orders mapping
- **بعد**: 31 خط با single TypeAdapter call
- ✅ رفع bug: File corruption
- ✅ تصحیح: `grpcRequest.DeliveryStatus = request.Status.Value`
##### **Validators (2 فایل)**
14. **PlaceOrderCommandValidator.cs**
- ❌ حذف شد: Validation برای `GatewayAmount` (فیلد وجود ندارد)
- ❌ حذف شد: Validation برای مجموع مبالغ
- ✅ باقیمانده: Validation برای `DiscountBalanceAmount`
15. **CompleteOrderPaymentCommandValidator.cs**
- ❌ حذف شد: Validation برای `PaidAmount` (فیلد وجود ندارد)
- ✅ تغییر: TransactionCode فقط وقتی PaymentSuccess=true الزامی است
##### **Interfaces (2 فایل)**
16. **IApplicationContractContext.cs**
- **قبل**: `using CMSMicroservice.Protobuf.Protos.DiscountOrder;`
- **بعد**: `using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;`
- اصلاح: Property type برای `DiscountOrders`
17. **ApplicationContractContext.cs**
- **قبل**: `using CMSMicroservice.Protobuf.Protos.DiscountOrder;`
- **بعد**: `using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;`
##### **Project File (1 فایل)**
18. **BackOffice.BFF.Application.csproj**
- ✅ اضافه شد: `<ProjectReference>` به DiscountOrder.Protobuf
- ❌ حذف شد: `<Compile Remove="DiscountOrderCQ/**/*.cs" />`
---
## 🏗️ تصمیمات معماری
### 1. استفاده از BFF Protobuf (نه CMS Proto)
**قانون**: در لایه WebApi و Application از BackOffice.BFF، تنها باید از BFF Protobuf استفاده شود.
```csharp
// ❌ اشتباه
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
// ✅ صحیح
using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
```
**دلیل**: جداسازی Contract ها و امکان تغییرات مستقل
### 2. Protobuf StringValue Handling
**کشف**: کامپایلر Protobuf به صورت خودکار `string` را به `StringValue` تبدیل می‌کند.
```csharp
// ❌ قبلاً فکر می‌کردیم نیاز است
Notes = !string.IsNullOrEmpty(src.Notes)
? new StringValue { Value = src.Notes }
: null
// ✅ کامپایلر خودش handle می‌کند
Notes = src.Notes
```
### 3. Expression Tree Lambda محدودیت‌ها
**مشکل**: در Mapster نمی‌توان از null propagating operator استفاده کرد.
```csharp
// ❌ خطا: CS8072
CreatedAt = order.Created?.ToDateTime() ?? DateTime.UtcNow
// ✅ صحیح
CreatedAt = order.Created != null ? order.Created.ToDateTime() : DateTime.UtcNow
```
### 4. MetaData Property Naming
**کشف**: Proto از `current_page`/`total_page` استفاده می‌کند، نه `PageNumber`/`TotalPages`.
```csharp
// Application/Common/Models/MetaData.cs
public class MetaData
{
public long CurrentPage { get; set; } // نه PageNumber
public long TotalPage { get; set; } // نه TotalPages
public long PageSize { get; set; }
public long TotalCount { get; set; }
public bool HasPrevious { get; set; }
public bool HasNext { get; set; }
}
```
---
## 🎯 الگوهای Mapster پیاده‌سازی شده
### الگوی 1: Command به Proto Request
```csharp
config.NewConfig<PlaceOrderCommand, PlaceOrderRequest>()
.MapWith(src => new PlaceOrderRequest
{
UserId = src.UserId,
UserAddressId = src.AddressId,
DiscountBalanceToUse = src.DiscountBalanceAmount,
Notes = src.Notes // Auto-conversion to StringValue
});
```
### الگوی 2: Proto Response به DTO با محاسبات
```csharp
config.NewConfig<PlaceOrderResponse, PlaceOrderResponseDto>()
.MapWith(src => new PlaceOrderResponseDto
{
OrderId = src.OrderId,
TrackingCode = src.OrderId.ToString(),
RequiresGatewayPayment = src.GatewayAmount > 0, // محاسبه شده
GatewayPayableAmount = src.GatewayAmount
});
```
### الگوی 3: Enum Conversion
```csharp
config.NewConfig<UpdateOrderStatusCommand, UpdateOrderStatusRequest>()
.MapWith(src => new UpdateOrderStatusRequest
{
OrderId = src.OrderId,
DeliveryStatus = (DeliveryStatus)src.NewStatus, // int to enum
TrackingCode = src.TrackingCode,
AdminNotes = src.AdminNote
});
```
### الگوی 4: Complex Object با Helper Methods
```csharp
config.NewConfig<GetOrderByIdResponse, GetOrderByIdResponseDto>()
.MapWith(src => new GetOrderByIdResponseDto
{
// ... fields
ShippingAddress = src.Address != null ? new AddressInfoDto
{
Id = src.Address.Id,
RecipientName = src.Address.Title,
Province = ExtractProvince(src.Address.Address), // Helper
City = ExtractCity(src.Address.Address), // Helper
PostalCode = src.Address.PostalCode,
FullAddress = src.Address.Address
} : null
});
// Helper Method
private static string ExtractProvince(string fullAddress)
{
var parts = fullAddress?.Split(',');
return parts?.Length > 0 ? parts[0].Trim() : string.Empty;
}
```
### الگوی 5: Collection Mapping با LINQ
```csharp
Items = src.Items.Select(item => new Application.DiscountOrderCQ.Queries.GetOrderById.OrderItemDto
{
Id = item.ProductId,
ProductId = item.ProductId,
ProductTitle = item.ProductTitle,
UnitPrice = item.UnitPrice,
DiscountPercent = item.MaxDiscountPercent,
Quantity = item.Count,
TotalPrice = item.TotalPrice,
DiscountedPrice = item.FinalPrice
}).ToList()
```
---
## 🐛 مشکلات رفع شده
### مشکل 1: Type Conversion Errors (5 خطا)
**علت**: Interface از CMS Proto استفاده می‌کرد ولی Handler ها BFF Proto می‌فرستادند
**راه حل**:
```csharp
// IApplicationContractContext.cs
- using CMSMicroservice.Protobuf.Protos.DiscountOrder;
+ using BackOffice.BFF.DiscountOrder.Protobuf.Protos.DiscountOrder;
```
### مشکل 2: Missing Properties (3 خطا)
**علت**: Validator ها به فیلدهای حذف شده اشاره داشتند
**راه حل**:
- حذف validation برای `GatewayAmount` از PlaceOrderCommandValidator
- حذف validation برای `PaidAmount` از CompleteOrderPaymentCommandValidator
- اضافه کردن `UserId` به GetOrderByIdQuery
### مشکل 3: File Corruption (2 فایل)
**علت**: استفاده از multi_replace_string_in_file بدون include کردن closing braces کامل
**راه حل**: Replace کامل محتوای handler ها با کد صحیح
### مشکل 4: StringValue Conversion (4 خطا)
**علت**: تلاش برای manual wrapping در `new StringValue { Value = ... }`
**راه حل**: اجازه دادن به کامپایلر Protobuf برای auto-conversion
### مشکل 5: OrderItemDto Ambiguity (1 خطا)
**علت**: دو کلاس با نام یکسان (Proto و Application)
**راه حل**: استفاده از fully qualified name
```csharp
new Application.DiscountOrderCQ.Queries.GetOrderById.OrderItemDto { ... }
```
### مشکل 6: MetaData Property Names (2 خطا)
**علت**: استفاده از `PageNumber`/`TotalPages` به جای `CurrentPage`/`TotalPage`
**راه حل**: استفاده از property names صحیح Application MetaData
### مشکل 7: Null Propagating Operator (2 خطا)
**علت**: استفاده از `?.` در expression tree lambda
**راه حل**:
```csharp
- CreatedAt = src.Created?.ToDateTime() ?? DateTime.UtcNow
+ CreatedAt = src.Created != null ? src.Created.ToDateTime() : DateTime.UtcNow
```
---
## 📊 Mapster Profiles ایجاد شده
### 1. ClubMembershipProfile.cs
- GetClubMembership mappings
- GetClubMembershipUser mappings
### 2. CommissionProfile.cs
- GetNetworkCommissionCalculation mappings
- GetUserBalances mappings
### 3. ConfigurationProfile.cs
- GetAllConfigurations mappings
- GetConfiguration mappings
### 4. CategoryProfile.cs
- Category CRUD mappings
- Proto ↔ DTO conversions
### 5. ManualPaymentProfile.cs
- ProcessWithdrawal mappings
- GetPendingWithdrawals mappings
### 6. DiscountOrderProfile.cs (Application)
- PlaceOrder: Command → Proto Request/Response
- CompleteOrderPayment: Command → Proto Request/Response
- UpdateOrderStatus: Command → Proto Request/Response
- GetOrderById: Proto Response → DTO (Complex)
- GetUserOrders: Proto Response → DTO (با Pagination)
### 7. DiscountOrderProfile.cs (WebApi)
- همه mappings بالا برای لایه WebApi
- مدیریت StringValue و Timestamp
- Helper methods برای Persian enum titles
---
## 🔍 نکات کلیدی یادگرفته شده
### 1. Mapster Configuration
```csharp
// در Application layer
TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly());
// استفاده در Handler
var result = TypeAdapter.Adapt(source, source.GetType(), typeof(Destination));
```
### 2. Proto Field Naming Convention
- Proto: `snake_case` (e.g., `user_id`, `created_at`)
- C# Generated: `PascalCase` (e.g., `UserId`, `CreatedAt`)
- Compiler handles conversion automatically
### 3. Timestamp Handling
```csharp
// Proto timestamp to C# DateTime
CreatedAt = src.Created != null ? src.Created.ToDateTime() : DateTime.UtcNow
```
### 4. Enum در Proto vs C#
```proto
enum DeliveryStatus {
DELIVERY_PENDING = 0;
DELIVERY_PROCESSING = 1;
// ...
}
```
```csharp
// در C#
public enum DeliveryStatus {
DeliveryPending = 0,
DeliveryProcessing = 1,
// ...
}
```
### 5. Optional Fields
- Proto3: همه فیلدها optional هستند (nullable)
- `google.protobuf.StringValue`: برای nullable string
- `google.protobuf.Int32Value`: برای nullable int
---
## ✅ وضعیت نهایی
### Build Status
```
Build succeeded.
0 Error(s)
23 Warning(s)
Time Elapsed 00:00:02.26
```
### Handler های فعال
- ✅ ClubMembershipCQ
- ✅ CommissionCQ
- ✅ ConfigurationCQ
- ✅ CategoryCQ
- ✅ ManualPaymentCQ
- ✅ DiscountOrderCQ
- ✅ ProcessWithdrawal
- ❌ DiscountShoppingCartCQ (FrontOffice-only)
### Proto References
تمام BFF Protobuf projects به Application.csproj اضافه شدند:
```xml
<ProjectReference Include="..\Protobufs\BackOffice.BFF.Package.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.UserOrder.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.Commission.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.NetworkMembership.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.ClubMembership.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.Configuration.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.ManualPayment.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.DiscountOrder.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.Health.Protobuf\" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.PublicMessage.Protobuf\" />
```
---
## 📈 آمار نهایی
| متریک | مقدار |
|-------|-------|
| کل Handler های بررسی شده | 3 |
| Handler های فعال شده | 2 |
| Handler های FrontOffice-only | 1 |
| فایل‌های اصلاح شده | 21 |
| Mapster Profile های ایجاد شده | 7 |
| خطوط کد حذف شده | ~250 |
| خطوط کد اضافه شده | ~400 |
| کاهش complexity | ~60% |
| زمان کل | ~155 دقیقه |
| Build Errors قبل | 29 |
| Build Errors بعد | 0 ✅ |
---
## 🚀 مزایای حاصل شده
### 1. کد تمیزتر
- حذف manual field mapping (50-80 خط → 1 خط)
- کاهش code duplication
- Readability بهتر
### 2. Maintainability بالاتر
- تغییرات Proto به راحتی sync می‌شوند
- Profile های متمرکز
- کمتر احتمال خطا
### 3. Performance بهتر
- Mapster از compile-time code generation استفاده می‌کند
- سریعتر از reflection-based mappers
- Memory efficient
### 4. Type Safety
- Compile-time checking
- خطاهای mapping در build شناسایی می‌شوند
- IDE IntelliSense support
---
## 📝 توصیه‌ها برای آینده
### 1. Testing
```csharp
[Fact]
public void PlaceOrderCommand_Should_Map_To_PlaceOrderRequest()
{
// Arrange
var command = new PlaceOrderCommand { ... };
// Act
var request = command.Adapt<PlaceOrderRequest>();
// Assert
request.UserId.Should().Be(command.UserId);
request.UserAddressId.Should().Be(command.AddressId);
}
```
### 2. Custom Converters
برای logic های پیچیده‌تر می‌توان custom converter نوشت:
```csharp
config.NewConfig<Source, Dest>()
.Map(dest => dest.Field, src => CustomConverter(src.Field));
```
### 3. Validation Integration
ترکیب Mapster با FluentValidation:
```csharp
var command = request.Adapt<PlaceOrderCommand>();
var validationResult = await _validator.ValidateAsync(command);
if (!validationResult.IsValid) { ... }
```
### 4. Logging
اضافه کردن logging برای track کردن mapping issues:
```csharp
TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true;
TypeAdapterConfig.GlobalSettings.RequireDestinationMemberSource = true;
```
---
## 🎓 درس‌های آموخته شده
1. **Architecture First**: قبل از کد زدن، معماری را مشخص کنید (BFF Proto vs CMS Proto)
2. **Incremental Changes**: تغییرات را به صورت تدریجی انجام دهید و بعد از هر مرحله build کنید
3. **Read Proto Files**: همیشه فایل .proto را بخوانید تا structure دقیق را بدانید
4. **Compiler Is Smart**: به قابلیت‌های auto-conversion کامپایلر اعتماد کنید
5. **Expression Trees Have Limits**: محدودیت‌های expression tree lambda را بشناسید
6. **Fully Qualified Names**: در صورت ambiguity از نام کامل استفاده کنید
7. **Test After Each Fix**: بعد از هر تغییر مهم build کنید
8. **Document Decisions**: تصمیمات معماری را مستند کنید
---
## ✨ نتیجه‌گیری
پروژه BackOffice.BFF با موفقیت به Mapster مهاجرت داده شد. تمامی Handler های ضروری فعال و آماده استفاده هستند. کد حاصل شده:
- ✅ تمیزتر و خواناتر
- ✅ قابل نگهداری‌تر
- ✅ Type-safe
- ✅ بدون خطای Build
**وضعیت**: آماده برای Production 🚀
---
*این گزارش توسط Masoud و GitHub Copilot در تاریخ December 8, 2025 تهیه شده است.*
@@ -5,22 +5,10 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Exclude handlers that need proto fixes -->
<!-- All handlers now ENABLED with BFF Protobuf and Mapster profiles -->
<!-- ProcessWithdrawal, DiscountOrder, DiscountShoppingCart fixed and enabled -->
<ItemGroup>
<!-- DiscountOrder handlers - proto mismatch with CMS -->
<Compile Remove="DiscountOrderCQ/**/*.cs" />
<!-- DiscountShoppingCart handlers - proto mismatch -->
<Compile Remove="DiscountShoppingCartCQ/**/*.cs" />
<!-- ManualPayment handlers - WellKnownTypes conversion issues -->
<Compile Remove="ManualPaymentCQ/**/*.cs" />
<!-- Configuration handlers - StringValue conversion -->
<Compile Remove="ConfigurationCQ/**/*.cs" />
<!-- Commission ProcessWithdrawal - StringValue conversion -->
<Compile Remove="CommissionCQ/Commands/ProcessWithdrawal/**/*.cs" />
<!-- All excluded handlers have been enabled -->
</ItemGroup>
<ItemGroup>
@@ -40,6 +28,8 @@
<ProjectReference Include="..\Protobufs\BackOffice.BFF.NetworkMembership.Protobuf\BackOffice.BFF.NetworkMembership.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.ClubMembership.Protobuf\BackOffice.BFF.ClubMembership.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.Configuration.Protobuf\BackOffice.BFF.Configuration.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.ManualPayment.Protobuf\BackOffice.BFF.ManualPayment.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.DiscountOrder.Protobuf\BackOffice.BFF.DiscountOrder.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.Health.Protobuf\BackOffice.BFF.Health.Protobuf.csproj" />
<ProjectReference Include="..\Protobufs\BackOffice.BFF.PublicMessage.Protobuf\BackOffice.BFF.PublicMessage.Protobuf.csproj" />
</ItemGroup>
@@ -1,4 +1,5 @@
using CMSMicroservice.Protobuf.Protos.ClubMembership;
namespace BackOffice.BFF.Application.ClubMembershipCQ.Commands.ActivateClub;
@@ -1,6 +1,6 @@
using Foursat.BackOffice.BFF.ClubMembership.Protobuf;
using GetAllClubMembershipsRequest = CMSMicroservice.Protobuf.Protos.ClubMembership.GetAllClubMembershipsRequest;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
namespace BackOffice.BFF.Application.ClubMembershipCQ.Queries.GetAllClubMembers;
@@ -1,4 +1,5 @@
using CMSMicroservice.Protobuf.Protos.ClubMembership;
namespace BackOffice.BFF.Application.ClubMembershipCQ.Queries.GetClubStatistics;
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Commands.ApproveWithdrawal;
@@ -7,13 +7,18 @@ public record ProcessWithdrawalCommand : IRequest<ProcessWithdrawalResponseDto>
/// </summary>
public long WithdrawalId { get; init; }
/// <summary>
/// تایید یا رد درخواست
/// </summary>
public bool IsApproved { get; init; }
/// <summary>
/// دلیل رد یا یادداشت مدیر (اختیاری)
/// </summary>
public string? Reason { get; init; }
/// <summary>
/// شناسه تراکنش بانکی (اختیاری)
/// </summary>
public string? TransactionId { get; init; }
/// <summary>
/// یادداشت مدیر (اختیاری)
/// </summary>
public string? AdminNote { get; init; }
}
@@ -1,5 +1,4 @@
using BackOffice.BFF.Commission.Protobuf;
using Google.Protobuf.WellKnownTypes;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Commands.ProcessWithdrawal;
@@ -17,18 +16,20 @@ public class ProcessWithdrawalCommandHandler : IRequestHandler<ProcessWithdrawal
var grpcRequest = new ProcessWithdrawalRequest
{
PayoutId = request.WithdrawalId,
IsApproved = true,
Reason = request.AdminNote != null
? new StringValue { Value = request.AdminNote }
: null
IsApproved = request.IsApproved,
Reason = request.Reason ?? string.Empty
};
await _context.Commissions.ProcessWithdrawalAsync(grpcRequest, cancellationToken: cancellationToken);
var statusMessage = request.IsApproved
? "درخواست برداشت تایید و برای پرداخت ارسال شد"
: "درخواست برداشت رد شد";
return new ProcessWithdrawalResponseDto
{
Success = true,
Message = "درخواست برداشت برای پرداخت ارسال شد"
Message = statusMessage
};
}
}
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Commands.RejectWithdrawal;
@@ -2,7 +2,7 @@ namespace BackOffice.BFF.Application.CommissionCQ.Commands.TriggerWeeklyCalculat
public class TriggerWeeklyCalculationCommand : IRequest<TriggerWeeklyCalculationResponseDto>
{
public string WeekNumber { get; set; } = string.Empty;
public long WeekDefinitionId { get; set; }
public bool ForceRecalculate { get; set; }
public bool SkipBalances { get; set; }
public bool SkipPool { get; set; }
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Commands.TriggerWeeklyCalculation;
@@ -15,7 +17,7 @@ public class TriggerWeeklyCalculationCommandHandler : IRequestHandler<TriggerWee
{
var grpcRequest = new TriggerWeeklyCalculationRequest
{
WeekNumber = request.WeekNumber,
WeekDefinitionId = request.WeekDefinitionId,
ForceRecalculate = request.ForceRecalculate,
SkipBalances = request.SkipBalances,
SkipPool = request.SkipPool,
@@ -1,4 +1,7 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetAllWeeklyPools;
@@ -9,7 +9,8 @@ public record GetAllWeeklyPoolsResponseDto
public record WeeklyCommissionPoolDto
{
public long Id { get; init; }
public string WeekNumber { get; init; } = string.Empty;
public long WeekDefinitionId { get; init; }
public string WeekDisplayName { get; init; } = string.Empty;
public long TotalPoolAmount { get; init; }
public int TotalBalances { get; init; }
public long ValuePerBalance { get; init; }
@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetAvailableWeeks;
public class GetAvailableWeeksQuery : IRequest<GetAvailableWeeksResponseDto>
{
public int FutureWeeksCount { get; init; } = 4;
public int PastWeeksCount { get; init; } = 12;
}
@@ -0,0 +1,54 @@
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetAvailableWeeks;
public class GetAvailableWeeksQueryHandler : IRequestHandler<GetAvailableWeeksQuery, GetAvailableWeeksResponseDto>
{
private readonly IApplicationContractContext _context;
public GetAvailableWeeksQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAvailableWeeksResponseDto> Handle(
GetAvailableWeeksQuery request,
CancellationToken cancellationToken)
{
// Call CMS microservice through BFF proto
var cmsRequest = new GetAvailableWeeksRequest
{
FutureWeeksCount = request.FutureWeeksCount,
PastWeeksCount = request.PastWeeksCount
};
var response = await _context.Commissions.GetAvailableWeeksAsync(cmsRequest, cancellationToken: cancellationToken);
// Map to DTO
return new GetAvailableWeeksResponseDto
{
CurrentWeek = MapToDto(response.CurrentWeek),
CalculatedWeeks = response.CalculatedWeeks.Select(MapToDto).ToList(),
PendingWeeks = response.PendingWeeks.Select(MapToDto).ToList(),
FutureWeeks = response.FutureWeeks.Select(MapToDto).ToList()
};
}
private static WeekInfoDto MapToDto(WeekInfo weekInfo)
{
return new WeekInfoDto
{
WeekDefinitionId = weekInfo.WeekDefinitionId,
DisplayName = weekInfo.DisplayName,
StartDate = weekInfo.StartDate?.ToDateTime() ?? DateTime.MinValue,
EndDate = weekInfo.EndDate?.ToDateTime() ?? DateTime.MinValue,
IsCalculated = weekInfo.IsCalculated,
CalculatedAt = weekInfo.CalculatedAt?.ToDateTime(),
LastExecutionStatus = weekInfo.LastExecutionStatus,
TotalPoolAmount = weekInfo.TotalPoolAmount != 0 ? weekInfo.TotalPoolAmount : null,
EligibleUsersCount = weekInfo.EligibleUsersCount != 0 ? weekInfo.EligibleUsersCount : null,
DisplayText = weekInfo.DisplayText
};
}
}
@@ -0,0 +1,23 @@
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetAvailableWeeks;
public class GetAvailableWeeksResponseDto
{
public required WeekInfoDto CurrentWeek { get; init; }
public required List<WeekInfoDto> CalculatedWeeks { get; init; }
public required List<WeekInfoDto> PendingWeeks { get; init; }
public required List<WeekInfoDto> FutureWeeks { get; init; }
}
public class WeekInfoDto
{
public required long WeekDefinitionId { get; init; }
public required string DisplayName { get; init; }
public required DateTime StartDate { get; init; }
public required DateTime EndDate { get; init; }
public bool IsCalculated { get; init; }
public DateTime? CalculatedAt { get; init; }
public string? LastExecutionStatus { get; init; }
public long? TotalPoolAmount { get; init; }
public int? EligibleUsersCount { get; init; }
public required string DisplayText { get; init; }
}
@@ -1,24 +1,37 @@
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetUserPayouts;
public record GetUserPayoutsQuery : IRequest<GetUserPayoutsResponseDto>
{
/// <summary>
/// موقعیت صفحه‌بندی
/// </summary>
public PaginationState? PaginationState { get; init; }
/// <summary>
/// مرتب‌سازی بر اساس
/// </summary>
public string? SortBy { get; init; }
/// <summary>
/// فیلتر
/// </summary>
public GetUserPayoutsFilter? Filter { get; init; }
}
public class GetUserPayoutsFilter
{
/// <summary>
/// شناسه کاربر
/// </summary>
public long UserId { get; init; }
public long? UserId { get; set; }
/// <summary>
/// شماره هفته (اختیاری - برای فیلتر)
/// شناسه هفته (اختیاری - برای فیلتر)
/// </summary>
public string? WeekNumber { get; init; }
public long? WeekDefinitionId { get; set; }
/// <summary>
/// شماره صفحه (پیش‌فرض: 1)
/// وضعیت (اختیاری)
/// </summary>
public int PageNumber { get; init; } = 1;
/// <summary>
/// تعداد در هر صفحه (پیش‌فرض: 10)
/// </summary>
public int PageSize { get; init; } = 10;
public int? Status { get; set; }
}
@@ -1,4 +1,4 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetUserPayouts;
@@ -13,10 +13,57 @@ public class GetUserPayoutsQueryHandler : IRequestHandler<GetUserPayoutsQuery, G
public async Task<GetUserPayoutsResponseDto> Handle(GetUserPayoutsQuery request, CancellationToken cancellationToken)
{
var cmsRequest = new GetUserCommissionPayoutsRequest
{
PageIndex = request.PaginationState?.PageNumber ?? 1,
PageSize = request.PaginationState?.PageSize ?? 10
};
if (request.Filter?.UserId.HasValue == true && request.Filter.UserId.Value > 0)
{
cmsRequest.UserId = request.Filter.UserId.Value;
}
if (request.Filter?.WeekDefinitionId.HasValue == true && request.Filter.WeekDefinitionId.Value > 0)
{
cmsRequest.WeekDefinitionId = request.Filter.WeekDefinitionId.Value;
}
if (request.Filter?.Status.HasValue == true)
{
cmsRequest.Status = request.Filter.Status.Value;
}
var response = await _context.Commissions.GetUserCommissionPayoutsAsync(
request.Adapt<GetUserCommissionPayoutsRequest>(),
cmsRequest,
cancellationToken: cancellationToken);
return response.Adapt<GetUserPayoutsResponseDto>();
var x= new GetUserPayoutsResponseDto
{
MetaData = new MetaData
{
CurrentPage = response.MetaData.CurrentPage,
PageSize = response.MetaData.PageSize,
TotalCount = response.MetaData.TotalCount,
TotalPage = response.MetaData.TotalPage
},
Models = response.Models.Select(m => new GetUserPayoutsResponseModel
{
Id = m.Id,
UserId = m.UserId,
UserName = m.UserName,
WeekDefinitionId = m.WeekDefinitionId,
WeekDisplayName = m.WeekDisplayName,
BalancesEarned = (int)m.BalancesEarned,
ValuePerBalance = m.ValuePerBalance,
TotalAmount = m.TotalAmount,
Status = m.Status,
WithdrawalMethod = m.WithdrawalMethod,
IbanNumber = m.IbanNumber,
Created = m.Created!=null?m.Created.ToDateTime():DateTime.Now,
// LastModified = m.LastModified?.ToDateTime()
}).ToList()
};
return x;
}
}
@@ -0,0 +1,24 @@
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetUserPayouts;
public class GetUserPayoutsQueryValidator : AbstractValidator<GetUserPayoutsQuery>
{
public GetUserPayoutsQueryValidator()
{
// RuleFor(x => x.PaginationState)
// .NotNull()
// .WithMessage("موقعیت صفحه‌بندی الزامی است");
//
// When(x => x.Filter != null, () =>
// {
// RuleFor(x => x.Filter!.UserId)
// .GreaterThan(0)
// .When(x => x.Filter!.UserId.HasValue)
// .WithMessage("شناسه کاربر معتبر نیست");
//
// RuleFor(x => x.Filter!.WeekNumber)
// .Matches(@"^\d{4}-W\d{2}$")
// .When(x => !string.IsNullOrWhiteSpace(x.Filter!.WeekNumber))
// .WithMessage("فرمت شماره هفته باید به صورت YYYY-Www باشد (مثال: 2025-W48)");
// });
}
}
@@ -3,27 +3,17 @@ namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetUserPayouts;
public class GetUserPayoutsResponseDto
{
/// <summary>
/// لیست Payout ها
/// متادیتا
/// </summary>
public List<UserPayoutDto> Payouts { get; set; } = new();
public MetaData MetaData { get; set; }
/// <summary>
/// تعداد کل رکوردها
/// مدل خروجی
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// شماره صفحه فعلی
/// </summary>
public int PageNumber { get; set; }
/// <summary>
/// تعداد در هر صفحه
/// </summary>
public int PageSize { get; set; }
public List<GetUserPayoutsResponseModel>? Models { get; set; }
}
public class UserPayoutDto
public class GetUserPayoutsResponseModel
{
/// <summary>
/// شناسه Payout
@@ -36,42 +26,57 @@ public class UserPayoutDto
public long UserId { get; set; }
/// <summary>
/// شماره هفته
/// نام کاربر
/// </summary>
public string WeekNumber { get; set; } = string.Empty;
public string UserName { get; set; } = string.Empty;
/// <summary>
/// بالانس Leg چپ
/// شناسه هفته
/// </summary>
public decimal LeftLegBalance { get; set; }
public long WeekDefinitionId { get; set; }
/// <summary>
/// بالانس Leg راست
/// نام نمایشی هفته
/// </summary>
public decimal RightLegBalance { get; set; }
public string WeekDisplayName { get; set; } = string.Empty;
/// <summary>
/// بالانس Leg ضعیف‌تر
/// تعداد تعادل‌های کسب شده
/// </summary>
public decimal WeakerLegBalance { get; set; }
public int BalancesEarned { get; set; }
/// <summary>
/// مبلغ کمیسیون (تومان)
/// ارزش هر تعادل
/// </summary>
public decimal CommissionAmount { get; set; }
public long ValuePerBalance { get; set; }
/// <summary>
/// وضعیت پرداخت
/// مبلغ کل
/// </summary>
public string Status { get; set; } = string.Empty;
public long TotalAmount { get; set; }
/// <summary>
/// تاریخ پرداخت
/// وضعیت (0=Pending, 1=Paid, 2=Failed)
/// </summary>
public DateTime? PaidAt { get; set; }
public int Status { get; set; }
/// <summary>
/// روش برداشت
/// </summary>
public int? WithdrawalMethod { get; set; }
/// <summary>
/// شماره شبا
/// </summary>
public string IbanNumber { get; set; } = string.Empty;
/// <summary>
/// تاریخ ایجاد
/// </summary>
public DateTime CreatedAt { get; set; }
public DateTime Created { get; set; }
/// <summary>
/// آخرین تغییر
/// </summary>
public DateTime? LastModified { get; set; }
}
@@ -3,27 +3,35 @@ namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
public record GetUserWeeklyBalancesQuery : IRequest<GetUserWeeklyBalancesResponseDto>
{
/// <summary>
/// شناسه کاربر (فیلتر اختیاری)
/// موقعیت صفحه‌بندی
/// </summary>
public long? UserId { get; init; }
public PaginationState? PaginationState { get; init; }
/// <summary>
/// شماره هفته (فیلتر اختیاری)
/// مرتب‌سازی بر اساس
/// </summary>
public string? WeekNumber { get; init; }
public string? SortBy { get; init; }
/// <summary>
/// فیلتر
/// </summary>
public GetUserWeeklyBalancesFilter? Filter { get; init; }
}
public class GetUserWeeklyBalancesFilter
{
/// <summary>
/// شناسه کاربر (فیلتر اختیاری)
/// </summary>
public long? UserId { get; set; }
/// <summary>
/// شناسه هفته (فیلتر اختیاری)
/// </summary>
public long? WeekDefinitionId { get; set; }
/// <summary>
/// فقط تعادل‌های فعال (منقضی نشده)
/// </summary>
public bool OnlyActive { get; init; }
/// <summary>
/// شماره صفحه
/// </summary>
public int PageIndex { get; init; } = 1;
/// <summary>
/// تعداد در صفحه
/// </summary>
public int PageSize { get; init; } = 10;
public bool? OnlyActive { get; set; }
}
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
@@ -15,19 +17,19 @@ public class GetUserWeeklyBalancesQueryHandler : IRequestHandler<GetUserWeeklyBa
{
var grpcRequest = new GetUserWeeklyBalancesRequest
{
OnlyActive = request.OnlyActive,
PageIndex = request.PageIndex,
PageSize = request.PageSize
OnlyActive = request.Filter?.OnlyActive ?? false,
PageIndex = request.PaginationState?.PageNumber ?? 1,
PageSize = request.PaginationState?.PageSize ?? 10
};
if (request.UserId.HasValue)
if (request.Filter?.UserId.HasValue == true && request.Filter.UserId.Value > 0)
{
grpcRequest.UserId = request.UserId.Value;
grpcRequest.UserId = request.Filter.UserId.Value;
}
if (!string.IsNullOrWhiteSpace(request.WeekNumber))
if (request.Filter?.WeekDefinitionId.HasValue == true && request.Filter.WeekDefinitionId.Value > 0)
{
grpcRequest.WeekNumber = request.WeekNumber;
grpcRequest.WeekDefinitionId = request.Filter.WeekDefinitionId.Value;
}
var response = await _context.Commissions.GetUserWeeklyBalancesAsync(grpcRequest, cancellationToken: cancellationToken);
@@ -0,0 +1,24 @@
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
public class GetUserWeeklyBalancesQueryValidator : AbstractValidator<GetUserWeeklyBalancesQuery>
{
public GetUserWeeklyBalancesQueryValidator()
{
RuleFor(x => x.PaginationState)
.NotNull()
.WithMessage("موقعیت صفحه‌بندی الزامی است");
When(x => x.Filter != null, () =>
{
RuleFor(x => x.Filter!.UserId)
.GreaterThan(0)
.When(x => x.Filter!.UserId.HasValue)
.WithMessage("شناسه کاربر معتبر نیست");
RuleFor(x => x.Filter!.WeekDefinitionId)
.GreaterThan(0)
.When(x => x.Filter!.WeekDefinitionId.HasValue)
.WithMessage("شناسه هفته معتبر نیست");
});
}
}
@@ -10,9 +10,15 @@ public record UserWeeklyBalanceDto
{
public long Id { get; init; }
public long UserId { get; init; }
public string WeekNumber { get; init; } = string.Empty;
public int LeftLegBalances { get; init; }
public int RightLegBalances { get; init; }
public string UserFullName { get; init; } = string.Empty;
public long WeekDefinitionId { get; init; }
public string WeekDisplayName { get; init; } = string.Empty;
public int LeftLegNewMembers { get; init; }
public int LeftLegCarryover { get; init; }
public int LeftLegTotal { get; init; }
public int RightLegNewMembers { get; init; }
public int RightLegCarryover { get; init; }
public int RightLegTotal { get; init; }
public int TotalBalances { get; init; }
public long WeeklyPoolContribution { get; init; }
public DateTime? CalculatedAt { get; init; }
@@ -3,7 +3,7 @@ namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWeeklyPool;
public record GetWeeklyPoolQuery : IRequest<GetWeeklyPoolResponseDto>
{
/// <summary>
/// شماره هفته (فرمت: YYYY-Www مثلاً 2025-W48)
/// شناسه هفته
/// </summary>
public string WeekNumber { get; init; } = string.Empty;
public long WeekDefinitionId { get; init; }
}
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWeeklyPool;
@@ -2,58 +2,12 @@ namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWeeklyPool;
public class GetWeeklyPoolResponseDto
{
/// <summary>
/// شناسه Pool
/// </summary>
public long Id { get; set; }
/// <summary>
/// شماره هفته (فرمت: YYYY-Www)
/// </summary>
public string WeekNumber { get; set; } = string.Empty;
/// <summary>
/// مجموع کل Pool (تومان)
/// </summary>
public decimal TotalPoolValue { get; set; }
/// <summary>
/// مجموع مشارکت‌های اولیه (InitialContribution)
/// </summary>
public decimal TotalContributions { get; set; }
/// <summary>
/// مجموع Payout های پرداخت شده
/// </summary>
public decimal TotalPayouts { get; set; }
/// <summary>
/// باقیمانده Pool
/// </summary>
public decimal LeftBalance { get; set; }
/// <summary>
/// تعداد اعضای فعال در این هفته
/// </summary>
public int ActiveMembersCount { get; set; }
/// <summary>
/// آیا محاسبه شده است؟
/// </summary>
public long WeekDefinitionId { get; set; }
public long TotalPoolAmount { get; set; }
public long TotalBalances { get; set; }
public decimal ValuePerBalance { get; set; }
public bool IsCalculated { get; set; }
/// <summary>
/// تاریخ محاسبه
/// </summary>
public DateTime? CalculatedAt { get; set; }
/// <summary>
/// تاریخ ایجاد
/// </summary>
public DateTime CreatedAt { get; set; }
/// <summary>
/// تاریخ آخرین ویرایش
/// </summary>
public DateTime? ModifiedAt { get; set; }
public DateTimeOffset Created { get; set; }
}
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWithdrawalReports;
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWithdrawalRequests;
@@ -35,6 +37,35 @@ public class GetWithdrawalRequestsQueryHandler : IRequestHandler<GetWithdrawalRe
}
var response = await _context.Commissions.GetWithdrawalRequestsAsync(grpcRequest, cancellationToken: cancellationToken);
return response.Adapt<GetWithdrawalRequestsResponseDto>();
return new GetWithdrawalRequestsResponseDto
{
MetaData = new MetaDataDto
{
TotalCount = (int)(response.MetaData?.TotalCount ?? 0),
PageSize = (int)(response.MetaData?.PageSize ?? request.PageSize),
CurrentPage = (int)(response.MetaData?.CurrentPage ?? request.PageIndex),
TotalPages = (int)(response.MetaData?.TotalPage ?? 0)
},
Models = response.Models.Select(m => new WithdrawalRequestDto
{
Id = m.Id,
UserId = m.UserId,
UserName = m.UserName,
WeekDefinitionId = m.WeekDefinitionId,
Amount = m.Amount,
Status = m.Status,
WithdrawalMethod = m.WithdrawalMethod,
IbanNumber = m.IbanNumber,
RequestedAt = m.RequestedAt?.ToDateTime() ?? DateTime.MinValue,
ProcessedAt = m.ProcessedAt?.ToDateTime(),
ProcessedBy = m.ProcessedBy,
Reason = m.Reason,
Created = m.Created?.ToDateTime() ?? DateTime.MinValue,
BankReferenceId = m.BankReferenceId,
BankTrackingCode = m.BankTrackingCode,
PaymentFailureReason = m.PaymentFailureReason
}).ToList()
};
}
}
@@ -11,18 +11,19 @@ public record WithdrawalRequestDto
public long Id { get; init; }
public long UserId { get; init; }
public string UserName { get; init; } = string.Empty;
public string PhoneNumber { get; init; } = string.Empty;
public long WeekDefinitionId { get; init; }
public long Amount { get; init; }
public int Status { get; init; }
public string Method { get; init; } = "Bank";
public string? BankAccount { get; init; }
public string? BankName { get; init; }
public int WithdrawalMethod { get; init; }
public string? IbanNumber { get; init; }
public DateTime RequestedAt { get; init; }
public DateTime? ProcessedAt { get; init; }
public string? ProcessedBy { get; init; }
public string? Reason { get; init; }
public DateTime Created { get; init; }
public string? BankReferenceId { get; init; }
public string? BankTrackingCode { get; init; }
public string? PaymentFailureReason { get; init; }
public DateTime RequestDate { get; init; }
public DateTime? ProcessedDate { get; init; }
public string? AdminNote { get; init; }
}
public record MetaDataDto
@@ -2,7 +2,7 @@ namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWorkerExecutionLogs
public class GetWorkerExecutionLogsQuery : IRequest<GetWorkerExecutionLogsResponseDto>
{
public string? WeekNumber { get; set; }
public long? WeekDefinitionId { get; set; }
public string? ExecutionId { get; set; }
public bool? SuccessOnly { get; set; }
public bool? FailedOnly { get; set; }
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWorkerExecutionLogs;
@@ -19,9 +21,9 @@ public class GetWorkerExecutionLogsQueryHandler : IRequestHandler<GetWorkerExecu
PageSize = request.PageSize
};
if (!string.IsNullOrWhiteSpace(request.WeekNumber))
if (request.WeekDefinitionId.HasValue && request.WeekDefinitionId.Value > 0)
{
grpcRequest.WeekNumber = request.WeekNumber;
grpcRequest.WeekDefinitionId = request.WeekDefinitionId.Value;
}
if (!string.IsNullOrWhiteSpace(request.ExecutionId))
@@ -9,7 +9,8 @@ public class GetWorkerExecutionLogsResponseDto
public class WorkerExecutionLogModel
{
public string ExecutionId { get; set; } = string.Empty;
public string WeekNumber { get; set; } = string.Empty;
public long WeekDefinitionId { get; set; }
public string WeekDisplayName { get; set; } = string.Empty;
public string Step { get; set; } = string.Empty;
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
@@ -1,4 +1,6 @@
using BackOffice.BFF.Commission.Protobuf;
using CMSMicroservice.Protobuf.Protos.Commission;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWorkerStatus;
@@ -9,17 +9,22 @@ using CMSMicroservice.Protobuf.Protos.ProductImages;
using CMSMicroservice.Protobuf.Protos.ProductGalleries;
using CMSMicroservice.Protobuf.Protos.Category;
using CMSMicroservice.Protobuf.Protos.ProductCategory;
using BackOffice.BFF.NetworkMembership.Protobuf;
using FMSMicroservice.Protobuf.Protos.FileInfo;
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using CMSMicroservice.Protobuf.Protos.Tag;
using CMSMicroservice.Protobuf.Protos.ProductTag;
using CMSMicroservice.Protobuf.Protos;
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using CMSMicroservice.Protobuf.Protos.ManualPayment;
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using CMSMicroservice.Protobuf.Protos.AppVersion;
using CMSMicroservice.Protobuf.Protos.Inventory;
namespace BackOffice.BFF.Application.Common.Interfaces;
@@ -42,7 +47,7 @@ public interface IApplicationContractContext
UserRoleContract.UserRoleContractClient UserRoles { get; }
// Network & Commission System
BackOffice.BFF.Commission.Protobuf.CommissionContract.CommissionContractClient Commissions { get; }
CommissionContract.CommissionContractClient Commissions { get; }
NetworkMembershipContract.NetworkMembershipContractClient NetworkMemberships { get; }
ClubMembershipContract.ClubMembershipContractClient ClubMemberships { get; }
ConfigurationContract.ConfigurationContractClient Configurations { get; }
@@ -60,8 +65,14 @@ public interface IApplicationContractContext
// Public Messages
PublicMessageContract.PublicMessageContractClient PublicMessages { get; }
// Manual Payments (Admin) - CMS gRPC
CMSMicroservice.Protobuf.Protos.ManualPayment.ManualPaymentContract.ManualPaymentContractClient ManualPayments { get; }
// Manual Payments (Admin) - BackOffice BFF gRPC
ManualPaymentContract.ManualPaymentContractClient ManualPayments { get; }
// App Version Management
AppVersionContract.AppVersionContractClient AppVersions { get; }
// Inventory Management System
InventoryContract.InventoryContractClient Inventory { get; }
#endregion
}
@@ -0,0 +1,81 @@
using BackOffice.BFF.Application.CommissionCQ.Queries.GetWeeklyPool;
using BackOffice.BFF.Application.CommissionCQ.Queries.GetAllWeeklyPools;
using BackOffice.BFF.Application.CommissionCQ.Queries.GetUserWeeklyBalances;
using CMSMicroservice.Protobuf.Protos.Commission;
using WeeklyPoolsMetaDataDto = BackOffice.BFF.Application.CommissionCQ.Queries.GetAllWeeklyPools.MetaDataDto;
using UserBalancesMetaDataDto = BackOffice.BFF.Application.CommissionCQ.Queries.GetUserWeeklyBalances.MetaDataDto;
namespace BackOffice.BFF.Application.Common.Mappings;
public class CommissionProfile : IRegister
{
void IRegister.Register(TypeAdapterConfig config)
{
config.NewConfig<GetWeeklyCommissionPoolResponse, GetWeeklyPoolResponseDto>()
.Map(dest => dest.Id, src => src.Id)
.Map(dest => dest.CalculatedAt, src => src.CalculatedAt)
.Map(dest => dest.IsCalculated, src => src.IsCalculated)
.Map(dest => dest.WeekDefinitionId, src => src.WeekDefinitionId)
.Map(dest => dest.TotalPoolAmount, src => src.TotalPoolAmount)
.Map(dest => dest.ValuePerBalance, src => src.ValuePerBalance)
.Map(dest => dest.TotalBalances, src => src.TotalBalances)
.Map(dest => dest.Created, src => src.Created.ToDateTimeOffset());
// CMS GetAllWeeklyPoolsResponse -> GetAllWeeklyPoolsResponseDto
config.NewConfig<GetAllWeeklyPoolsResponse, GetAllWeeklyPoolsResponseDto>()
.MapWith(src => new GetAllWeeklyPoolsResponseDto
{
MetaData = new WeeklyPoolsMetaDataDto
{
TotalCount = (int)src.MetaData.TotalCount,
PageSize = (int)src.MetaData.PageSize,
CurrentPage = (int)src.MetaData.CurrentPage,
TotalPages = (int)src.MetaData.TotalPage
},
Models = src.Models.Select(m => new WeeklyCommissionPoolDto
{
Id = m.Id,
WeekDefinitionId = m.WeekDefinitionId,
WeekDisplayName = m.WeekDisplayName,
TotalPoolAmount = m.TotalPoolAmount,
TotalBalances = m.TotalBalances,
ValuePerBalance = m.ValuePerBalance,
IsCalculated = m.IsCalculated,
CalculatedAt = m.CalculatedAt != null ? m.CalculatedAt.ToDateTime() : null,
Created = m.Created.ToDateTime()
}).ToList()
});
// CMS GetUserWeeklyBalancesResponse -> GetUserWeeklyBalancesResponseDto
config.NewConfig<GetUserWeeklyBalancesResponse, GetUserWeeklyBalancesResponseDto>()
.MapWith(src => new GetUserWeeklyBalancesResponseDto
{
MetaData = new UserBalancesMetaDataDto
{
TotalCount = (int)src.MetaData.TotalCount,
PageSize = (int)src.MetaData.PageSize,
CurrentPage = (int)src.MetaData.CurrentPage,
TotalPages = (int)src.MetaData.TotalPage
},
Models = src.Models.Select(m => new UserWeeklyBalanceDto
{
Id = m.Id,
UserId = m.UserId,
UserFullName = m.UserFullName ?? string.Empty,
WeekDefinitionId = m.WeekDefinitionId,
WeekDisplayName = m.WeekDisplayName ?? string.Empty,
LeftLegNewMembers = m.LeftLegNewMembers,
LeftLegCarryover = m.LeftLegCarryover,
LeftLegTotal = m.LeftLegTotal,
RightLegNewMembers = m.RightLegNewMembers,
RightLegCarryover = m.RightLegCarryover,
RightLegTotal = m.RightLegTotal,
TotalBalances = m.TotalBalances,
WeeklyPoolContribution = m.WeeklyPoolContribution,
CalculatedAt = m.CalculatedAt != null ? m.CalculatedAt.ToDateTime() : null,
IsExpired = m.IsExpired,
Created = m.Created.ToDateTime()
}).ToList()
});
}
}
@@ -51,6 +51,9 @@ public class GeneralMapping : IRegister
config.NewConfig<TimeSpan?, Duration>()
.MapWith(src => src.HasValue ? Duration.FromTimeSpan(src.Value) : null);
config.NewConfig<DateTimeOffset?, DateTime?>()
.MapWith(src => src.HasValue ? src.Value.DateTime : (DateTime?)null);
config.Default
.UseDestinationValue(member => member.SetterModifier == AccessModifier.None &&
member.Type.IsGenericType &&
@@ -2,7 +2,8 @@ namespace BackOffice.BFF.Application.Common.Models;
public static class RoleNames
{
public const string SuperAdmin = "SuperAdmin";
// public const string SuperAdmin = "SuperAdmin";
public const string SuperAdmin = "Administrator";
public const string Admin = "Admin";
public const string Inspector = "Inspector";
}
@@ -1,5 +1,6 @@
using BackOffice.BFF.Configuration.Protobuf;
using Google.Protobuf.WellKnownTypes;
using CMSMicroservice.Protobuf.Protos.Configuration;
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.CreateOrUpdateConfiguration;
@@ -1,4 +1,5 @@
using BackOffice.BFF.Configuration.Protobuf;
using CMSMicroservice.Protobuf.Protos.Configuration;
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.DeactivateConfiguration;
@@ -1,7 +1,5 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.Configuration.Protobuf;
using Google.Protobuf.WellKnownTypes;
using MediatR;
using CMSMicroservice.Protobuf.Protos.Configuration;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.SetDefaultVatPercentage;
@@ -27,10 +25,7 @@ public class SetDefaultVatPercentageCommandHandler : IRequestHandler<SetDefaultV
Key = VatConfigurationKey,
Value = request.VatPercentage.ToString(),
Scope = 0, // System scope
Description = new StringValue
{
Value = "درصد پیش‌فرض مالیات بر ارزش افزوده سفارش‌ها"
}
Description = "درصد پیش‌فرض مالیات بر ارزش افزوده سفارش‌ها"
};
await _context.Configurations.CreateOrUpdateConfigurationAsync(grpcRequest, cancellationToken: cancellationToken);
@@ -1,5 +1,3 @@
using FluentValidation;
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.SetDefaultVatPercentage;
public class SetDefaultVatPercentageCommandValidator : AbstractValidator<SetDefaultVatPercentageCommand>
@@ -0,0 +1,39 @@
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.UpdateAppVersion;
public class UpdateAppVersionCommand : IRequest<Unit>
{
/// <summary>
/// نام اپلیکیشن (FrontOffice, BackOffice, Mobile)
/// </summary>
public string AppName { get; set; } = string.Empty;
/// <summary>
/// ورژن فعلی
/// </summary>
public string CurrentVersion { get; set; } = string.Empty;
/// <summary>
/// حداقل ورژن مورد نیاز
/// </summary>
public string? MinRequiredVersion { get; set; }
/// <summary>
/// آیا نیاز به پاک کردن کامل کش دارد؟
/// </summary>
public bool RequiresFullCacheClear { get; set; }
/// <summary>
/// پیام آپدیت برای کاربران
/// </summary>
public string? UpdateMessage { get; set; }
/// <summary>
/// یادداشت‌های نسخه
/// </summary>
public string? ReleaseNotes { get; set; }
/// <summary>
/// دلیل آپدیت (برای لاگ ادمین)
/// </summary>
public string? UpdateReason { get; set; }
}
@@ -0,0 +1,39 @@
using CMSMicroservice.Protobuf.Protos.AppVersion;
namespace BackOffice.BFF.Application.ConfigurationCQ.Commands.UpdateAppVersion;
public class UpdateAppVersionCommandHandler : IRequestHandler<UpdateAppVersionCommand, Unit>
{
private readonly IApplicationContractContext _context;
public UpdateAppVersionCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(UpdateAppVersionCommand request, CancellationToken cancellationToken)
{
var cmsRequest = new UpdateAppVersionRequest
{
AppName = request.AppName,
CurrentVersion = request.CurrentVersion,
RequiresFullCacheClear = request.RequiresFullCacheClear
};
if (!string.IsNullOrEmpty(request.MinRequiredVersion))
cmsRequest.MinRequiredVersion = request.MinRequiredVersion;
if (!string.IsNullOrEmpty(request.UpdateMessage))
cmsRequest.UpdateMessage = request.UpdateMessage;
if (!string.IsNullOrEmpty(request.ReleaseNotes))
cmsRequest.ReleaseNotes = request.ReleaseNotes;
if (!string.IsNullOrEmpty(request.UpdateReason))
cmsRequest.UpdateReason = request.UpdateReason;
await _context.AppVersions.UpdateAppVersionAsync(cmsRequest, cancellationToken: cancellationToken);
return Unit.Value;
}
}
@@ -0,0 +1,6 @@
namespace BackOffice.BFF.Application.ConfigurationCQ.Queries.GetAllAppVersions;
public class GetAllAppVersionsQuery : IRequest<GetAllAppVersionsResponseDto>
{
public bool IncludeInactive { get; set; }
}
@@ -0,0 +1,40 @@
using CMSMicroservice.Protobuf.Protos.AppVersion;
namespace BackOffice.BFF.Application.ConfigurationCQ.Queries.GetAllAppVersions;
public class GetAllAppVersionsQueryHandler : IRequestHandler<GetAllAppVersionsQuery, GetAllAppVersionsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetAllAppVersionsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAllAppVersionsResponseDto> Handle(GetAllAppVersionsQuery request, CancellationToken cancellationToken)
{
var cmsRequest = new GetAllAppVersionsRequest
{
IncludeInactive = request.IncludeInactive
};
var response = await _context.AppVersions.GetAllAppVersionsAsync(cmsRequest, cancellationToken: cancellationToken);
return new GetAllAppVersionsResponseDto
{
Items = response.Items.Select(item => new AppVersionItemDto
{
Id = item.Id,
AppName = item.AppName,
CurrentVersion = item.CurrentVersion,
MinRequiredVersion = item.MinRequiredVersion,
RequiresFullCacheClear = item.RequiresFullCacheClear,
UpdateMessage = item.UpdateMessage,
ReleaseNotes = item.ReleaseNotes,
IsActive = item.IsActive,
Created = item.Created?.ToDateTimeOffset() ?? DateTimeOffset.MinValue,
LastModified = item.LastModified?.ToDateTimeOffset() ?? DateTimeOffset.MinValue
}).ToList()
};
}
}
@@ -0,0 +1,20 @@
namespace BackOffice.BFF.Application.ConfigurationCQ.Queries.GetAllAppVersions;
public class GetAllAppVersionsResponseDto
{
public List<AppVersionItemDto> Items { get; set; } = new();
}
public class AppVersionItemDto
{
public long Id { 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 string? UpdateMessage { get; set; }
public string? ReleaseNotes { get; set; }
public bool IsActive { get; set; }
public DateTimeOffset Created { get; set; }
public DateTimeOffset LastModified { get; set; }
}
@@ -1,4 +1,5 @@
using BackOffice.BFF.Configuration.Protobuf;
using CMSMicroservice.Protobuf.Protos.Configuration;
namespace BackOffice.BFF.Application.ConfigurationCQ.Queries.GetAllConfigurations;
@@ -15,18 +16,26 @@ public class GetAllConfigurationsQueryHandler : IRequestHandler<GetAllConfigurat
{
var grpcRequest = new GetAllConfigurationsRequest
{
PageIndex = request.PageIndex,
PageSize = request.PageSize
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = request.PageIndex,
PageSize = request.PageSize
}
};
if (request.Scope.HasValue)
if (request.Scope.HasValue || request.IsActive.HasValue)
{
grpcRequest.Scope = request.Scope.Value;
}
grpcRequest.Filter = new GetAllConfigurationsFilter();
if (request.Scope.HasValue)
{
grpcRequest.Filter.Scope = request.Scope.Value;
}
if (request.IsActive.HasValue)
{
grpcRequest.IsActive = request.IsActive.Value;
if (request.IsActive.HasValue)
{
grpcRequest.Filter.IsActive = request.IsActive.Value;
}
}
var response = await _context.Configurations.GetAllConfigurationsAsync(grpcRequest, cancellationToken: cancellationToken);
@@ -1,6 +1,4 @@
using BackOffice.BFF.Application.Common.Interfaces;
using BackOffice.BFF.Configuration.Protobuf;
using MediatR;
using CMSMicroservice.Protobuf.Protos.Configuration;
using Microsoft.Extensions.Logging;
namespace BackOffice.BFF.Application.ConfigurationCQ.Queries.GetCurrentVatPercentage;
@@ -14,6 +14,7 @@ public class CreateDiscountCategoryCommandValidator : AbstractValidator<CreateDi
.MaximumLength(200).WithMessage("عنوان دسته‌بندی نباید بیشتر از 200 کاراکتر باشد");
RuleFor(x => x.Description)
.MaximumLength(1000).WithMessage("توضیحات نباید بیشتر از 1000 کاراکتر باشد");
.MaximumLength(1000).WithMessage("توضیحات نباید بیشتر از 1000 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.Description));
}
}
@@ -17,7 +17,8 @@ public class UpdateDiscountCategoryCommandValidator : AbstractValidator<UpdateDi
.MaximumLength(200).WithMessage("عنوان دسته‌بندی نباید بیشتر از 200 کاراکتر باشد");
RuleFor(x => x.Description)
.MaximumLength(1000).WithMessage("توضیحات نباید بیشتر از 1000 کاراکتر باشد");
.MaximumLength(1000).WithMessage("توضیحات نباید بیشتر از 1000 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.Description));
RuleFor(x => x.ParentCategoryId)
.NotEqual(x => x.CategoryId).When(x => x.ParentCategoryId.HasValue)
@@ -25,7 +25,7 @@ public class GetDiscountCategoriesQueryHandler : IRequestHandler<GetDiscountCate
return new GetDiscountCategoriesResponseDto
{
Categories = response.Categories.Select(MapCategory).ToList()
Categories = response.Categories?.Select(MapCategory).ToList() ?? new List<DiscountCategoryTreeDto>()
};
}
@@ -36,14 +36,14 @@ public class GetDiscountCategoriesQueryHandler : IRequestHandler<GetDiscountCate
Id = node.Id,
Name = node.Name,
Title = node.Title,
Description = string.IsNullOrWhiteSpace(node.Description) ? node.Description : string.Empty,
ImagePath =string.IsNullOrWhiteSpace(node.ImagePath)? node.ImagePath : string.Empty,
ParentCategoryId = node.ParentCategoryId!=null? node.ParentCategoryId.Value : null,
Description = !string.IsNullOrWhiteSpace(node.Description) ? node.Description : string.Empty,
ImagePath = !string.IsNullOrWhiteSpace(node.ImagePath) ? node.ImagePath : string.Empty,
ParentCategoryId = node.ParentCategoryId != 0 ? node.ParentCategoryId : null,
SortOrder = node.SortOrder,
IsActive = node.IsActive,
ProductCount = node.ProductCount,
Created = DateTime.UtcNow, // Proto doesn't have created field
ChildCategories = node.Children.Select(MapCategory).ToList()
ChildCategories = node.Children?.Select(MapCategory).ToList() ?? new List<DiscountCategoryTreeDto>()
};
}
}
@@ -1,13 +1,13 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
public record CompleteOrderPaymentCommand : IRequest
public record CompleteOrderPaymentCommand : IRequest<CompleteOrderPaymentResponseDto>
{
/// <summary>شناسه سفارش</summary>
public long OrderId { get; init; }
/// <summary>کد تراکنش پرداخت</summary>
public string PaymentTransactionCode { get; init; }
public string? PaymentTransactionCode { get; init; }
/// <summary>مبلغ پرداخت شده</summary>
public long PaidAmount { get; init; }
/// <summary>وضعیت موفقیت پرداخت</summary>
public bool PaymentSuccess { get; init; }
}
@@ -1,8 +1,9 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderPaymentCommand>
public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderPaymentCommand, CompleteOrderPaymentResponseDto>
{
private readonly IApplicationContractContext _context;
@@ -11,17 +12,12 @@ public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderP
_context = context;
}
public async Task<Unit> Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken)
public async Task<CompleteOrderPaymentResponseDto> Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken)
{
await _context.DiscountOrders.CompleteOrderPaymentAsync(
new CompleteOrderPaymentRequest
{
OrderId = request.OrderId,
PaymentTransactionCode = request.PaymentTransactionCode,
PaidAmount = request.PaidAmount
},
cancellationToken: cancellationToken);
return Unit.Value;
var grpcRequest = request.Adapt<CompleteOrderPaymentRequest>();
var response = await _context.DiscountOrders.CompleteOrderPaymentAsync(grpcRequest, cancellationToken: cancellationToken);
return response.Adapt<CompleteOrderPaymentResponseDto>();
}
}
@@ -9,8 +9,5 @@ public class CompleteOrderPaymentCommandValidator : AbstractValidator<CompleteOr
RuleFor(x => x.PaymentTransactionCode)
.NotEmpty().WithMessage("کد تراکنش پرداخت الزامی است");
RuleFor(x => x.PaidAmount)
.GreaterThan(0).WithMessage("مبلغ پرداخت شده باید بیشتر از صفر باشد");
}
}
@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
public class CompleteOrderPaymentResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
}
@@ -11,6 +11,6 @@ public record PlaceOrderCommand : IRequest<PlaceOrderResponseDto>
/// <summary>مبلغ پرداخت از موجودی تخفیف</summary>
public long DiscountBalanceAmount { get; init; }
/// <summary>مبلغ پرداخت از درگاه</summary>
public long GatewayAmount { get; init; }
/// <summary>یادداشت سفارش (اختیاری)</summary>
public string? Notes { get; init; }
}
@@ -1,3 +1,4 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
@@ -13,22 +14,10 @@ public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, Place
public async Task<PlaceOrderResponseDto> Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
{
var response = await _context.DiscountOrders.PlaceOrderAsync(
new PlaceOrderRequest
{
UserId = request.UserId,
AddressId = request.AddressId,
DiscountBalanceAmount = request.DiscountBalanceAmount,
GatewayAmount = request.GatewayAmount
},
cancellationToken: cancellationToken);
var grpcRequest = request.Adapt<PlaceOrderRequest>();
return new PlaceOrderResponseDto
{
OrderId = response.OrderId,
TrackingCode = response.TrackingCode,
RequiresGatewayPayment = response.RequiresGatewayPayment,
GatewayPayableAmount = response.GatewayPayableAmount
};
var response = await _context.DiscountOrders.PlaceOrderAsync(grpcRequest, cancellationToken: cancellationToken);
return response.Adapt<PlaceOrderResponseDto>();
}
}
@@ -12,12 +12,5 @@ public class PlaceOrderCommandValidator : AbstractValidator<PlaceOrderCommand>
RuleFor(x => x.DiscountBalanceAmount)
.GreaterThanOrEqualTo(0).WithMessage("مبلغ موجودی تخفیف نمی‌تواند منفی باشد");
RuleFor(x => x.GatewayAmount)
.GreaterThanOrEqualTo(0).WithMessage("مبلغ درگاه نمی‌تواند منفی باشد");
RuleFor(x => x)
.Must(x => x.DiscountBalanceAmount + x.GatewayAmount > 0)
.WithMessage("مجموع مبالغ پرداخت باید بیشتر از صفر باشد");
}
}
@@ -1,13 +1,16 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
public record UpdateOrderStatusCommand : IRequest
public record UpdateOrderStatusCommand : IRequest<UpdateOrderStatusResponseDto>
{
/// <summary>شناسه سفارش</summary>
public long OrderId { get; init; }
/// <summary>وضعیت جدید</summary>
/// <summary>وضعیت جدید (DeliveryStatus enum)</summary>
public int NewStatus { get; init; }
/// <summary>کد رهگیری (اختیاری)</summary>
public string? TrackingCode { get; init; }
/// <summary>یادداشت مدیر (اختیاری)</summary>
public string AdminNote { get; init; }
public string? AdminNote { get; init; }
}
@@ -1,8 +1,9 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
public class UpdateOrderStatusCommandHandler : IRequestHandler<UpdateOrderStatusCommand>
public class UpdateOrderStatusCommandHandler : IRequestHandler<UpdateOrderStatusCommand, UpdateOrderStatusResponseDto>
{
private readonly IApplicationContractContext _context;
@@ -11,17 +12,12 @@ public class UpdateOrderStatusCommandHandler : IRequestHandler<UpdateOrderStatus
_context = context;
}
public async Task<Unit> Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken)
public async Task<UpdateOrderStatusResponseDto> Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken)
{
await _context.DiscountOrders.UpdateOrderStatusAsync(
new UpdateOrderStatusRequest
{
OrderId = request.OrderId,
NewStatus = request.NewStatus,
AdminNote = request.AdminNote ?? string.Empty
},
cancellationToken: cancellationToken);
return Unit.Value;
var grpcRequest = request.Adapt<UpdateOrderStatusRequest>();
var response = await _context.DiscountOrders.UpdateOrderStatusAsync(grpcRequest, cancellationToken: cancellationToken);
return response.Adapt<UpdateOrderStatusResponseDto>();
}
}
@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
public class UpdateOrderStatusResponseDto
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
}
@@ -0,0 +1,50 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetAllDiscountOrders;
/// <summary>
/// کوئری دریافت همه سفارشات فروشگاه تخفیفی برای ادمین
/// </summary>
public class GetAllDiscountOrdersQuery : IRequest<GetAllDiscountOrdersResponseDto>
{
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 20;
public long? UserId { get; set; }
public int? PaymentStatus { get; set; }
public int? DeliveryStatus { get; set; }
public string? UserMobile { get; set; }
public string? TrackingCode { get; set; }
public DateTime? FromDate { get; set; }
public DateTime? ToDate { get; set; }
public long? MinAmount { get; set; }
public long? MaxAmount { get; set; }
}
public class GetAllDiscountOrdersResponseDto
{
public MetaData MetaData { get; set; } = new();
public List<AdminOrderDto> Orders { get; set; } = new();
}
public class AdminOrderDto
{
public long Id { get; set; }
public long UserId { get; set; }
public string UserFullName { get; set; } = string.Empty;
public string UserMobile { get; set; } = string.Empty;
public long TotalAmount { get; set; }
public long DiscountBalanceUsed { get; set; }
public long GatewayAmountPaid { get; set; }
public long VatAmount { get; set; }
public int PaymentStatus { get; set; }
public DateTime? PaymentDate { get; set; }
public int DeliveryStatus { get; set; }
public DateTime? DeliveryDate { get; set; }
public string? ShippingAddress { get; set; }
public string? ReceiverName { get; set; }
public string? ReceiverMobile { get; set; }
public string? TrackingCode { get; set; }
public string? AdminNote { get; set; }
public DateTime Created { get; set; }
public DateTime? LastModified { get; set; }
public int ItemsCount { get; set; }
}
@@ -0,0 +1,88 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetAllDiscountOrders;
public class GetAllDiscountOrdersQueryHandler : IRequestHandler<GetAllDiscountOrdersQuery, GetAllDiscountOrdersResponseDto>
{
private readonly IApplicationContractContext _context;
public GetAllDiscountOrdersQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAllDiscountOrdersResponseDto> Handle(GetAllDiscountOrdersQuery request, CancellationToken cancellationToken)
{
var grpcRequest = new GetAllDiscountOrdersRequest
{
PageNumber = request.PageNumber,
PageSize = request.PageSize
};
if (request.UserId.HasValue)
grpcRequest.UserId = request.UserId.Value;
if (request.PaymentStatus.HasValue)
grpcRequest.PaymentStatus = request.PaymentStatus.Value;
if (request.DeliveryStatus.HasValue)
grpcRequest.DeliveryStatus = request.DeliveryStatus.Value;
if (!string.IsNullOrWhiteSpace(request.UserMobile))
grpcRequest.UserMobile = request.UserMobile;
if (!string.IsNullOrWhiteSpace(request.TrackingCode))
grpcRequest.TrackingCode = request.TrackingCode;
if (request.FromDate.HasValue)
grpcRequest.FromDate = Timestamp.FromDateTime(DateTime.SpecifyKind(request.FromDate.Value, DateTimeKind.Utc));
if (request.ToDate.HasValue)
grpcRequest.ToDate = Timestamp.FromDateTime(DateTime.SpecifyKind(request.ToDate.Value, DateTimeKind.Utc));
if (request.MinAmount.HasValue)
grpcRequest.MinAmount = request.MinAmount.Value;
if (request.MaxAmount.HasValue)
grpcRequest.MaxAmount = request.MaxAmount.Value;
var response = await _context.DiscountOrders.GetAllDiscountOrdersAsync(grpcRequest, cancellationToken: cancellationToken);
return new GetAllDiscountOrdersResponseDto
{
MetaData = new MetaData
{
CurrentPage = response.MetaData.CurrentPage,
TotalPage = response.MetaData.TotalPage,
PageSize = response.MetaData.PageSize,
TotalCount = response.MetaData.TotalCount,
HasPrevious = response.MetaData.HasPrevious,
HasNext = response.MetaData.HasNext
},
Orders = response.Models.Select(o => new AdminOrderDto
{
Id = o.Id,
UserId = o.UserId,
UserFullName = o.UserFullName,
UserMobile = o.UserMobile,
TotalAmount = o.TotalAmount,
DiscountBalanceUsed = o.DiscountBalanceUsed,
GatewayAmountPaid = o.GatewayAmountPaid,
VatAmount = o.VatAmount,
PaymentStatus = (int)o.PaymentStatus,
PaymentDate = o.PaymentDate?.ToDateTime(),
DeliveryStatus = (int)o.DeliveryStatus,
DeliveryDate = o.DeliveryDate?.ToDateTime(),
ShippingAddress = o.ShippingAddress,
ReceiverName = o.ReceiverName,
ReceiverMobile = o.ReceiverMobile,
TrackingCode = o.TrackingCode,
AdminNote = o.AdminNote,
Created = o.Created.ToDateTime(),
LastModified = o.LastModified?.ToDateTime(),
ItemsCount = o.ItemsCount
}).ToList()
};
}
}
@@ -0,0 +1,53 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetDiscountSalesReport;
public class GetDiscountSalesReportQuery : IRequest<DiscountSalesReportDto>
{
public DateTime FromDate { get; set; }
public DateTime ToDate { get; set; }
public ReportPeriodType PeriodType { get; set; } = ReportPeriodType.Daily;
}
public enum ReportPeriodType
{
Summary = 0,
Daily = 1,
Weekly = 2,
Monthly = 3
}
public class DiscountSalesReportDto
{
public DateTime FromDate { get; set; }
public DateTime ToDate { get; set; }
public ReportPeriodType PeriodType { get; set; }
public SalesSummaryDto Summary { get; set; } = new();
public List<PeriodSalesDto> Periods { get; set; } = new();
}
public class SalesSummaryDto
{
public int TotalOrders { get; set; }
public int SuccessfulOrders { get; set; }
public int PendingOrders { get; set; }
public int RejectedOrders { get; set; }
public decimal TotalRevenue { get; set; }
public decimal TotalDiscountUsed { get; set; }
public decimal TotalGatewayPayments { get; set; }
public decimal TotalVat { get; set; }
public int TotalItemsSold { get; set; }
public int UniqueCustomers { get; set; }
public decimal AverageOrderValue { get; set; }
}
public class PeriodSalesDto
{
public string PeriodLabel { get; set; } = string.Empty;
public DateTime PeriodStart { get; set; }
public DateTime PeriodEnd { get; set; }
public int OrderCount { get; set; }
public decimal Revenue { get; set; }
public decimal DiscountUsed { get; set; }
public decimal GatewayPayments { get; set; }
public decimal VatAmount { get; set; }
public int ItemsSold { get; set; }
}
@@ -0,0 +1,59 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetDiscountSalesReport;
public class GetDiscountSalesReportQueryHandler : IRequestHandler<GetDiscountSalesReportQuery, DiscountSalesReportDto>
{
private readonly IApplicationContractContext _context;
public GetDiscountSalesReportQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<DiscountSalesReportDto> Handle(GetDiscountSalesReportQuery request, CancellationToken cancellationToken)
{
var grpcRequest = new GetDiscountSalesReportRequest
{
FromDate = Timestamp.FromDateTime(DateTime.SpecifyKind(request.FromDate, DateTimeKind.Utc)),
ToDate = Timestamp.FromDateTime(DateTime.SpecifyKind(request.ToDate, DateTimeKind.Utc)),
ReportType = (SalesReportType)request.PeriodType
};
var response = await _context.DiscountOrders.GetDiscountSalesReportAsync(grpcRequest, cancellationToken: cancellationToken);
return new DiscountSalesReportDto
{
FromDate = request.FromDate,
ToDate = request.ToDate,
PeriodType = request.PeriodType,
Summary = new SalesSummaryDto
{
TotalOrders = response.Summary.TotalOrders,
SuccessfulOrders = response.Summary.CompletedOrders,
PendingOrders = response.Summary.PendingOrders,
RejectedOrders = response.Summary.CancelledOrders,
TotalRevenue = response.Summary.TotalSalesAmount,
TotalDiscountUsed = response.Summary.TotalDiscountUsed,
TotalGatewayPayments = response.Summary.TotalGatewayPaid,
TotalVat = response.Summary.TotalVatAmount,
TotalItemsSold = response.Summary.TotalProductsSold,
UniqueCustomers = response.Summary.UniqueCustomers,
AverageOrderValue = response.Summary.AverageOrderValue
},
Periods = response.Periods.Select(p => new PeriodSalesDto
{
PeriodLabel = p.PeriodLabel,
PeriodStart = p.PeriodStart.ToDateTime(),
PeriodEnd = p.PeriodEnd.ToDateTime(),
OrderCount = p.OrdersCount,
Revenue = p.TotalAmount,
DiscountUsed = p.DiscountUsed,
GatewayPayments = p.GatewayPaid,
VatAmount = 0, // Not in proto response
ItemsSold = 0 // Not in proto response
}).ToList()
};
}
}
@@ -4,4 +4,7 @@ public record GetOrderByIdQuery : IRequest<GetOrderByIdResponseDto>
{
/// <summary>شناسه سفارش</summary>
public long OrderId { get; init; }
/// <summary>شناسه کاربر (برای بررسی مجوز)</summary>
public long UserId { get; init; }
}
@@ -1,3 +1,4 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetOrderById;
@@ -13,46 +14,14 @@ public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, GetOr
public async Task<GetOrderByIdResponseDto> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
{
var response = await _context.DiscountOrders.GetOrderByIdAsync(
new GetOrderByIdRequest { OrderId = request.OrderId },
cancellationToken: cancellationToken);
return new GetOrderByIdResponseDto
{
Id = response.Id,
UserId = response.UserId,
TrackingCode = response.TrackingCode,
Status = response.Status,
StatusTitle = response.StatusTitle,
TotalPrice = response.TotalPrice,
DiscountBalanceUsed = response.DiscountBalanceUsed,
GatewayPayment = response.GatewayPayment,
FinalPrice = response.FinalPrice,
PaymentTransactionCode = response.PaymentTransactionCode,
AdminNote = response.AdminNote,
CreatedAt = response.CreatedAt.ToDateTime(),
PaidAt = response.HasPaidAt ? response.PaidAt.ToDateTime() : null,
ShippingAddress = new AddressInfoDto
{
Id = response.ShippingAddress.Id,
RecipientName = response.ShippingAddress.RecipientName,
RecipientPhone = response.ShippingAddress.RecipientPhone,
Province = response.ShippingAddress.Province,
City = response.ShippingAddress.City,
PostalCode = response.ShippingAddress.PostalCode,
FullAddress = response.ShippingAddress.FullAddress
},
Items = response.Items.Select(item => new OrderItemDto
{
Id = item.Id,
ProductId = item.ProductId,
ProductTitle = item.ProductTitle,
UnitPrice = item.UnitPrice,
DiscountPercent = item.DiscountPercent,
Quantity = item.Quantity,
TotalPrice = item.TotalPrice,
DiscountedPrice = item.DiscountedPrice
}).ToList()
var grpcRequest = new GetOrderByIdRequest
{
OrderId = request.OrderId,
UserId = request.UserId
};
var response = await _context.DiscountOrders.GetOrderByIdAsync(grpcRequest, cancellationToken: cancellationToken);
return response.Adapt<GetOrderByIdResponseDto>();
}
}
@@ -1,6 +1,5 @@
using BackOffice.BFF.Application.Common.Models;
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetUserOrders;
@@ -23,32 +22,10 @@ public class GetUserOrdersQueryHandler : IRequestHandler<GetUserOrdersQuery, Get
};
if (request.Status.HasValue)
grpcRequest.Status = new Int32Value { Value = request.Status.Value };
grpcRequest.DeliveryStatus = request.Status.Value;
var response = await _context.DiscountOrders.GetUserOrdersAsync(grpcRequest, cancellationToken: cancellationToken);
return new GetUserOrdersResponseDto
{
MetaData = new MetaData
{
CurrentPage = response.MetaData.CurrentPage,
TotalPage = response.MetaData.TotalPages,
PageSize = response.MetaData.PageSize,
TotalCount = response.MetaData.TotalCount,
HasPrevious = response.MetaData.HasPrevious,
HasNext = response.MetaData.HasNext
},
Orders = response.Orders.Select(order => new UserOrderDto
{
Id = order.Id,
TrackingCode = order.TrackingCode,
Status = order.Status,
StatusTitle = order.StatusTitle,
TotalPrice = order.TotalPrice,
FinalPrice = order.FinalPrice,
ItemCount = order.ItemCount,
CreatedAt = order.CreatedAt.ToDateTime()
}).ToList()
};
return response.Adapt<GetUserOrdersResponseDto>();
}
}
@@ -1,5 +1,3 @@
using BackOffice.BFF.Application.Common.Models;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetUserOrders;
public class GetUserOrdersResponseDto
@@ -0,0 +1,22 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.AddDiscountProductImage;
public class AddDiscountProductImageCommand : IRequest<AddDiscountProductImageResponseDto>
{
public long DiscountProductId { get; set; }
public ImageFileInput? ImageFile { get; set; }
public ImageFileInput? ThumbnailFile { get; set; }
public string? Title { get; set; }
public string? AltText { get; set; }
}
public class ImageFileInput
{
public byte[] Data { get; set; } = [];
public string FileName { get; set; } = string.Empty;
public string MimeType { get; set; } = string.Empty;
}
public class AddDiscountProductImageResponseDto
{
public long ImageId { get; set; }
}
@@ -0,0 +1,67 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using Google.Protobuf;
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.AddDiscountProductImage;
public class AddDiscountProductImageCommandHandler : IRequestHandler<AddDiscountProductImageCommand, AddDiscountProductImageResponseDto>
{
private readonly IApplicationContractContext _context;
public AddDiscountProductImageCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<AddDiscountProductImageResponseDto> Handle(AddDiscountProductImageCommand request, CancellationToken cancellationToken)
{
var addRequest = new AddDiscountProductImageRequest
{
DiscountProductId = request.DiscountProductId
};
if (!string.IsNullOrWhiteSpace(request.Title))
addRequest.Title = request.Title;
if (!string.IsNullOrWhiteSpace(request.AltText))
addRequest.AltText = request.AltText;
// Upload Image
if (request.ImageFile != null && request.ImageFile.Data != null && request.ImageFile.Data.Length > 0)
{
var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
{
Directory = "Images/DiscountShop/Products/Gallery",
IsBase64 = false,
MIME = request.ImageFile.MimeType,
FileName = request.ImageFile.FileName,
File = ByteString.CopyFrom(request.ImageFile.Data)
}, cancellationToken: cancellationToken);
if (imageFileInfo != null && !string.IsNullOrWhiteSpace(imageFileInfo.File))
addRequest.ImagePath = imageFileInfo.File;
}
// Upload Thumbnail
if (request.ThumbnailFile != null && request.ThumbnailFile.Data != null && request.ThumbnailFile.Data.Length > 0)
{
var thumbnailFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
{
Directory = "Images/DiscountShop/Products/Gallery/Thumbnails",
IsBase64 = false,
MIME = request.ThumbnailFile.MimeType,
FileName = request.ThumbnailFile.FileName,
File = ByteString.CopyFrom(request.ThumbnailFile.Data)
}, cancellationToken: cancellationToken);
if (thumbnailFileInfo != null && !string.IsNullOrWhiteSpace(thumbnailFileInfo.File))
addRequest.ThumbnailPath = thumbnailFileInfo.File;
}
var response = await _context.DiscountProducts.AddDiscountProductImageAsync(addRequest, cancellationToken: cancellationToken);
return new AddDiscountProductImageResponseDto
{
ImageId = response.ImageId
};
}
}
@@ -17,9 +17,6 @@ public record CreateDiscountProductCommand : IRequest<CreateDiscountProductRespo
/// <summary>حداکثر درصد تخفیف (0-100)</summary>
public int MaxDiscountPercent { get; init; }
/// <summary>تعداد اولیه موجودی</summary>
public int InitialCount { get; init; }
/// <summary>ترتیب نمایش</summary>
public int SortOrder { get; init; }
@@ -17,11 +17,10 @@ public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscoun
var createRequest = new CreateDiscountProductRequest
{
Title = request.Title,
ShortInfomation = request.ShortInformation,
FullInformation = request.FullInformation,
ShortInfomation = request.ShortInformation ?? string.Empty,
FullInformation = request.FullInformation ?? string.Empty,
Price = request.Price,
MaxDiscountPercent = request.MaxDiscountPercent,
InitialCount = request.InitialCount,
SortOrder = request.SortOrder,
IsActive = request.IsActive
};
@@ -9,11 +9,12 @@ public class CreateDiscountProductCommandValidator : AbstractValidator<CreateDis
.MaximumLength(200).WithMessage("عنوان محصول نباید بیشتر از 200 کاراکتر باشد");
RuleFor(x => x.ShortInformation)
.NotEmpty().WithMessage("اطلاعات کوتاه محصول الزامی است")
.MaximumLength(500).WithMessage("اطلاعات کوتاه نباید بیشتر از 500 کاراکتر باشد");
.MaximumLength(500).WithMessage("اطلاعات کوتاه نباید بیشتر از 500 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.ShortInformation));
RuleFor(x => x.FullInformation)
.NotEmpty().WithMessage("اطلاعات کامل محصول الزامی است");
.MaximumLength(10000).WithMessage("اطلاعات کامل نباید بیشتر از 10000 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.FullInformation));
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("قیمت محصول باید بیشتر از صفر باشد");
@@ -21,9 +22,6 @@ public class CreateDiscountProductCommandValidator : AbstractValidator<CreateDis
RuleFor(x => x.MaxDiscountPercent)
.InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
RuleFor(x => x.InitialCount)
.GreaterThanOrEqualTo(0).WithMessage("تعداد اولیه نمی‌تواند منفی باشد");
RuleFor(x => x.CategoryIds)
.NotEmpty().WithMessage("حداقل یک دسته‌بندی باید انتخاب شود");
}
@@ -0,0 +1,6 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.DeleteDiscountProductImage;
public class DeleteDiscountProductImageCommand : IRequest
{
public long Id { get; set; }
}
@@ -0,0 +1,23 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.DeleteDiscountProductImage;
public class DeleteDiscountProductImageCommandHandler : IRequestHandler<DeleteDiscountProductImageCommand>
{
private readonly IApplicationContractContext _context;
public DeleteDiscountProductImageCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteDiscountProductImageCommand request, CancellationToken cancellationToken)
{
await _context.DiscountProducts.DeleteDiscountProductImageAsync(new DeleteDiscountProductImageRequest
{
Id = request.Id
}, cancellationToken: cancellationToken);
return Unit.Value;
}
}
@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.ReorderDiscountProductImages;
public class ReorderDiscountProductImagesCommand : IRequest
{
public long DiscountProductId { get; set; }
public List<long> ImageIds { get; set; } = new();
}
@@ -0,0 +1,26 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.ReorderDiscountProductImages;
public class ReorderDiscountProductImagesCommandHandler : IRequestHandler<ReorderDiscountProductImagesCommand>
{
private readonly IApplicationContractContext _context;
public ReorderDiscountProductImagesCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(ReorderDiscountProductImagesCommand request, CancellationToken cancellationToken)
{
var reorderRequest = new ReorderDiscountProductImagesRequest
{
DiscountProductId = request.DiscountProductId
};
reorderRequest.ImageIds.AddRange(request.ImageIds);
await _context.DiscountProducts.ReorderDiscountProductImagesAsync(reorderRequest, cancellationToken: cancellationToken);
return Unit.Value;
}
}
@@ -18,8 +18,8 @@ public class UpdateDiscountProductCommandHandler : IRequestHandler<UpdateDiscoun
{
ProductId = request.ProductId,
Title = request.Title,
ShortInfomation = request.ShortInformation,
FullInformation = request.FullInformation,
ShortInfomation = request.ShortInformation ?? string.Empty,
FullInformation = request.FullInformation ?? string.Empty,
Price = request.Price,
MaxDiscountPercent = request.MaxDiscountPercent,
SortOrder = request.SortOrder,
@@ -12,11 +12,12 @@ public class UpdateDiscountProductCommandValidator : AbstractValidator<UpdateDis
.MaximumLength(200).WithMessage("عنوان محصول نباید بیشتر از 200 کاراکتر باشد");
RuleFor(x => x.ShortInformation)
.NotEmpty().WithMessage("اطلاعات کوتاه محصول الزامی است")
.MaximumLength(500).WithMessage("اطلاعات کوتاه نباید بیشتر از 500 کاراکتر باشد");
.MaximumLength(500).WithMessage("اطلاعات کوتاه نباید بیشتر از 500 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.ShortInformation));
RuleFor(x => x.FullInformation)
.NotEmpty().WithMessage("اطلاعات کامل محصول الزامی است");
.MaximumLength(10000).WithMessage("اطلاعات کامل نباید بیشتر از 10000 کاراکتر باشد")
.When(x => !string.IsNullOrEmpty(x.FullInformation));
RuleFor(x => x.Price)
.GreaterThan(0).WithMessage("قیمت محصول باید بیشتر از صفر باشد");
@@ -0,0 +1,13 @@
using BackOffice.BFF.Application.DiscountProductCQ.Commands.AddDiscountProductImage;
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.UpdateDiscountProductImage;
public class UpdateDiscountProductImageCommand : IRequest
{
public long Id { get; set; }
public ImageFileInput? ImageFile { get; set; }
public ImageFileInput? ThumbnailFile { get; set; }
public string? Title { get; set; }
public string? AltText { get; set; }
public bool IsActive { get; set; }
}
@@ -0,0 +1,65 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using Google.Protobuf;
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.UpdateDiscountProductImage;
public class UpdateDiscountProductImageCommandHandler : IRequestHandler<UpdateDiscountProductImageCommand>
{
private readonly IApplicationContractContext _context;
public UpdateDiscountProductImageCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(UpdateDiscountProductImageCommand request, CancellationToken cancellationToken)
{
var updateRequest = new UpdateDiscountProductImageRequest
{
Id = request.Id,
IsActive = request.IsActive
};
if (!string.IsNullOrWhiteSpace(request.Title))
updateRequest.Title = request.Title;
if (!string.IsNullOrWhiteSpace(request.AltText))
updateRequest.AltText = request.AltText;
// Upload new Image if provided
if (request.ImageFile != null && request.ImageFile.Data != null && request.ImageFile.Data.Length > 0)
{
var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
{
Directory = "Images/DiscountShop/Products/Gallery",
IsBase64 = false,
MIME = request.ImageFile.MimeType,
FileName = request.ImageFile.FileName,
File = ByteString.CopyFrom(request.ImageFile.Data)
}, cancellationToken: cancellationToken);
if (imageFileInfo != null && !string.IsNullOrWhiteSpace(imageFileInfo.File))
updateRequest.ImagePath = imageFileInfo.File;
}
// Upload new Thumbnail if provided
if (request.ThumbnailFile != null && request.ThumbnailFile.Data != null && request.ThumbnailFile.Data.Length > 0)
{
var thumbnailFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
{
Directory = "Images/DiscountShop/Products/Gallery/Thumbnails",
IsBase64 = false,
MIME = request.ThumbnailFile.MimeType,
FileName = request.ThumbnailFile.FileName,
File = ByteString.CopyFrom(request.ThumbnailFile.Data)
}, cancellationToken: cancellationToken);
if (thumbnailFileInfo != null && !string.IsNullOrWhiteSpace(thumbnailFileInfo.File))
updateRequest.ThumbnailPath = thumbnailFileInfo.File;
}
await _context.DiscountProducts.UpdateDiscountProductImageAsync(updateRequest, cancellationToken: cancellationToken);
return Unit.Value;
}
}
@@ -0,0 +1,24 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductImages;
public class GetDiscountProductImagesQuery : IRequest<GetDiscountProductImagesResponseDto>
{
public long DiscountProductId { get; set; }
public bool OnlyActive { get; set; } = false; // Admin can see all
}
public class GetDiscountProductImagesResponseDto
{
public List<DiscountProductImageDto> Images { get; set; } = new();
}
public class DiscountProductImageDto
{
public long Id { get; set; }
public long DiscountProductId { get; set; }
public string ImagePath { get; set; } = string.Empty;
public string? ThumbnailPath { get; set; }
public string? Title { get; set; }
public string? AltText { get; set; }
public int SortOrder { get; set; }
public bool IsActive { get; set; }
}
@@ -0,0 +1,39 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductImages;
public class GetDiscountProductImagesQueryHandler : IRequestHandler<GetDiscountProductImagesQuery, GetDiscountProductImagesResponseDto>
{
private readonly IApplicationContractContext _context;
public GetDiscountProductImagesQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetDiscountProductImagesResponseDto> Handle(GetDiscountProductImagesQuery request, CancellationToken cancellationToken)
{
var grpcRequest = new GetDiscountProductImagesRequest
{
DiscountProductId = request.DiscountProductId,
OnlyActive = request.OnlyActive
};
var response = await _context.DiscountProducts.GetDiscountProductImagesAsync(grpcRequest, cancellationToken: cancellationToken);
return new GetDiscountProductImagesResponseDto
{
Images = response.Images.Select(i => new DiscountProductImageDto
{
Id = i.Id,
DiscountProductId = i.DiscountProductId,
ImagePath = i.ImagePath,
ThumbnailPath = i.ThumbnailPath,
Title = i.Title,
AltText = i.AltText,
SortOrder = i.SortOrder,
IsActive = i.IsActive
}).ToList()
};
}
}
@@ -41,7 +41,7 @@ public class GetDiscountProductsQueryHandler : IRequestHandler<GetDiscountProduc
return new GetDiscountProductsResponseDto
{
MetaData = new MetaData
MetaData = response.MetaData != null ? new MetaData
{
CurrentPage = response.MetaData.CurrentPage,
TotalPage = response.MetaData.TotalPage,
@@ -49,8 +49,8 @@ public class GetDiscountProductsQueryHandler : IRequestHandler<GetDiscountProduc
TotalCount = response.MetaData.TotalCount,
HasPrevious = response.MetaData.HasPrevious,
HasNext = response.MetaData.HasNext
},
Products = response.Models.Select(p => new DiscountProductDto
} : new MetaData(),
Products = response.Models?.Select(p => new DiscountProductDto
{
Id = p.Id,
Title = p.Title,
@@ -62,8 +62,8 @@ public class GetDiscountProductsQueryHandler : IRequestHandler<GetDiscountProduc
RemainingCount = p.RemainingCount,
ViewCount = p.ViewCount,
IsActive = p.IsActive,
Created = p.Created.ToDateTime()
}).ToList()
Created = p.Created?.ToDateTime() ?? DateTime.MinValue
}).ToList() ?? new List<DiscountProductDto>()
};
}
}
@@ -5,6 +5,6 @@ public record RemoveFromCartCommand : IRequest
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
/// <summary>شناسه آیتم سبد خرید</summary>
public long CartItemId { get; init; }
/// <summary>شناسه محصول</summary>
public long ProductId { get; init; }
}
@@ -17,7 +17,7 @@ public class RemoveFromCartCommandHandler : IRequestHandler<RemoveFromCartComman
new RemoveFromCartRequest
{
UserId = request.UserId,
CartItemId = request.CartItemId
ProductId = request.ProductId
},
cancellationToken: cancellationToken);
@@ -7,7 +7,7 @@ public class RemoveFromCartCommandValidator : AbstractValidator<RemoveFromCartCo
RuleFor(x => x.UserId)
.GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
RuleFor(x => x.CartItemId)
.GreaterThan(0).WithMessage("شناسه آیتم سبد خرید نامعتبر است");
RuleFor(x => x.ProductId)
.GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
}
}
@@ -5,8 +5,8 @@ public record UpdateCartItemCountCommand : IRequest
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
/// <summary>شناسه آیتم سبد خرید</summary>
public long CartItemId { get; init; }
/// <summary>شناسه محصول</summary>
public long ProductId { get; init; }
/// <summary>تعداد جدید</summary>
public int NewCount { get; init; }
@@ -17,7 +17,7 @@ public class UpdateCartItemCountCommandHandler : IRequestHandler<UpdateCartItemC
new UpdateCartItemCountRequest
{
UserId = request.UserId,
CartItemId = request.CartItemId,
ProductId = request.ProductId,
NewCount = request.NewCount
},
cancellationToken: cancellationToken);
@@ -7,8 +7,8 @@ public class UpdateCartItemCountCommandValidator : AbstractValidator<UpdateCartI
RuleFor(x => x.UserId)
.GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
RuleFor(x => x.CartItemId)
.GreaterThan(0).WithMessage("شناسه آیتم سبد خرید نامعتبر است");
RuleFor(x => x.ProductId)
.GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
RuleFor(x => x.NewCount)
.GreaterThan(0).WithMessage("تعداد جدید باید بیشتر از صفر باشد");
@@ -19,13 +19,11 @@ public class GetUserCartQueryHandler : IRequestHandler<GetUserCartQuery, GetUser
return new GetUserCartResponseDto
{
UserId = response.UserId,
TotalPrice = response.TotalPrice,
TotalDiscountedPrice = response.TotalDiscountedPrice,
TotalSavings = response.TotalSavings,
TotalDiscountAmount = response.TotalDiscountAmount,
FinalPrice = response.FinalPrice,
Items = response.Items.Select(item => new CartItemDto
{
Id = item.Id,
ProductId = item.ProductId,
ProductTitle = item.ProductTitle,
ProductImagePath = item.ProductImagePath,
@@ -33,8 +31,10 @@ public class GetUserCartQueryHandler : IRequestHandler<GetUserCartQuery, GetUser
MaxDiscountPercent = item.MaxDiscountPercent,
Count = item.Count,
TotalPrice = item.TotalPrice,
DiscountedPrice = item.DiscountedPrice,
AddedAt = item.AddedAt.ToDateTime()
DiscountAmount = item.DiscountAmount,
FinalPrice = item.FinalPrice,
ProductRemainingCount = item.ProductRemainingCount,
Created = item.Created.ToDateTime()
}).ToList()
};
}
@@ -2,23 +2,28 @@ namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
public class GetUserCartResponseDto
{
public long UserId { get; set; }
public List<CartItemDto> Items { get; set; } = new();
/// <summary>جمع کل قیمت (بدون تخفیف)</summary>
public long TotalPrice { get; set; }
public long TotalDiscountedPrice { get; set; }
public long TotalSavings { get; set; }
/// <summary>جمع تخفیف‌ها</summary>
public long TotalDiscountAmount { get; set; }
/// <summary>قیمت نهایی (با تخفیف)</summary>
public long FinalPrice { get; set; }
}
public class CartItemDto
{
public long Id { get; set; }
public long ProductId { get; set; }
public string ProductTitle { get; set; }
public string ProductImagePath { get; set; }
public string ProductTitle { get; set; } = string.Empty;
public string ProductImagePath { get; set; } = string.Empty;
public long UnitPrice { get; set; }
public int MaxDiscountPercent { get; set; }
public int Count { get; set; }
public long TotalPrice { get; set; }
public long DiscountedPrice { get; set; }
public DateTime AddedAt { get; set; }
/// <summary>مبلغ تخفیف</summary>
public long DiscountAmount { get; set; }
/// <summary>قیمت نهایی آیتم</summary>
public long FinalPrice { get; set; }
public int ProductRemainingCount { get; set; }
public DateTime Created { get; set; }
}
@@ -1,5 +1,9 @@
using CMSMicroservice.Protobuf.Protos.ClubMembership;
using CMSMicroservice.Protobuf.Protos.Commission;
using CMSMicroservice.Protobuf.Protos.Configuration;
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
using Foursat.BackOffice.BFF.Health.Protobuf;
namespace BackOffice.BFF.Application.HealthCQ.Queries.GetSystemHealth;
@@ -25,7 +29,7 @@ public class GetSystemHealthQueryHandler : IRequestHandler<GetSystemHealthQuery,
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
await _context.Commissions.GetAllWeeklyPoolsAsync(
new BackOffice.BFF.Commission.Protobuf.GetAllWeeklyPoolsRequest
new GetAllWeeklyPoolsRequest
{
PageIndex = 1,
PageSize = 1
@@ -59,8 +63,11 @@ public class GetSystemHealthQueryHandler : IRequestHandler<GetSystemHealthQuery,
await _context.Configurations.GetAllConfigurationsAsync(
new GetAllConfigurationsRequest
{
PageIndex = 1,
PageSize = 1
PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
{
PageNumber = 1,
PageSize = 1
}
},
cancellationToken: cancellationToken);
stopwatch.Stop();
@@ -89,7 +96,7 @@ public class GetSystemHealthQueryHandler : IRequestHandler<GetSystemHealthQuery,
{
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
await _context.NetworkMemberships.GetNetworkStatisticsAsync(
new BackOffice.BFF.NetworkMembership.Protobuf.GetNetworkStatisticsRequest(),
new GetNetworkStatisticsRequest(),
cancellationToken: cancellationToken);
stopwatch.Stop();
networkHealthy = true;
@@ -0,0 +1,17 @@
namespace BackOffice.BFF.Application.InventoryCQ.Commands.AddStock;
public record AddStockCommand : IRequest<AddStockResponseDto>
{
public long ProductId { get; init; }
public int ProductType { get; init; } // 1=RegularProduct, 2=DiscountProduct
public int Quantity { get; init; }
public string? ReferenceNumber { get; init; }
public string? Note { get; init; }
public long? WarehouseId { get; init; }
}
public class AddStockResponseDto
{
public long InventoryItemId { get; set; }
public int NewQuantity { get; set; }
}

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