Compare commits
34 Commits
25476ba120
...
9d2b5ad2d4
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d2b5ad2d4 | |||
| 8940362575 | |||
| bca3b7fe62 | |||
| c03b3705c3 | |||
| a9140bec04 | |||
| 17c2958b9c | |||
| 2e54aa7aca | |||
| 7b26073c63 | |||
| 28e94d6137 | |||
| c6ee356cbd | |||
| 2d09a69be9 | |||
| 7cebb27171 | |||
| 68489a8374 | |||
| 330f0cae5a | |||
| b16f01b6e4 | |||
| 13f24d523b | |||
| 41308e189a | |||
| 8953d86755 | |||
| ef8d5d4d97 | |||
| 288b85a59b | |||
| 30144a07c8 | |||
| 5bcf6c9840 | |||
| 9d51e70a7f | |||
| a31c88219f | |||
| ceeb29d843 | |||
| 0ba6af9049 | |||
| 5ad69cb3fa | |||
| e383c64e5b | |||
| f5642f88fb | |||
| 14b5db645b | |||
| 14b4022de9 | |||
| d24f326fbc | |||
| 50ba9e3794 | |||
| c5915bb074 |
@@ -4,7 +4,7 @@ name: Push nuget and docker image Actions Workflow
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- stage
|
- stage_new
|
||||||
jobs:
|
jobs:
|
||||||
Deploy:
|
Deploy:
|
||||||
runs-on: windows
|
runs-on: windows
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
name: Build and Deploy to Production
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- production
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.foursat.afrino.co
|
||||||
|
IMAGE_NAME: admin/cms
|
||||||
|
|
||||||
|
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,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
|
||||||
|
run: |
|
||||||
|
mkdir -p /etc/docker
|
||||||
|
cat > /etc/docker/daemon.json << 'DAEMON'
|
||||||
|
{
|
||||||
|
"insecure-registries": ["git.foursat.afrino.co", "gitea-svc:3000"]
|
||||||
|
}
|
||||||
|
DAEMON
|
||||||
|
mkdir -p ~/.docker
|
||||||
|
cat > ~/.docker/config.json << 'CONF'
|
||||||
|
{
|
||||||
|
"proxies": {
|
||||||
|
"default": {
|
||||||
|
"httpProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
||||||
|
"httpsProxy": "http://proxyuser:87zH26nbqT2@46.249.98.211:3128",
|
||||||
|
"noProxy": "localhost,127.0.0.1,gitea-svc,45.149.79.127,10.0.0.0/8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONF
|
||||||
|
dockerd &
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
docker info >/dev/null 2>&1 && break || sleep 2
|
||||||
|
done
|
||||||
|
docker info
|
||||||
|
|
||||||
|
- name: Checkout code
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch production http://gitea-svc:3000/admin/CMS.git .
|
||||||
|
git log -1 --format="%H %s"
|
||||||
|
|
||||||
|
- name: Build Docker Image
|
||||||
|
run: |
|
||||||
|
docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
|
||||||
|
-t ${{ env.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 "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ env.REGISTRY }} -u admin --password-stdin
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
|
||||||
|
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:prod
|
||||||
|
|
||||||
|
- name: Deploy to Production
|
||||||
|
run: |
|
||||||
|
# Setup kubeconfig for PRODUCTION
|
||||||
|
mkdir -p ~/.kube
|
||||||
|
echo "${{ secrets.KUBECONFIG_PROD }}" | base64 -d > ~/.kube/config
|
||||||
|
|
||||||
|
# Restart deployment to pull new image
|
||||||
|
kubectl rollout restart deployment/cms || echo "Deployment doesn't exist yet"
|
||||||
|
|
||||||
|
# Wait for rollout to complete
|
||||||
|
kubectl rollout status deployment/cms --timeout=5m || echo "Deployment rollout pending"
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
{
|
||||||
|
"info": {
|
||||||
|
"_postman_id": "a5404a90-7d5a-4e9f-a26b-bb2bc93f19a2",
|
||||||
|
"name": "hushyar",
|
||||||
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
||||||
|
"_exporter_id": "12336342",
|
||||||
|
"_collection_link": "https://abbas-rahimzadeh-postman.postman.co/workspace/public-workspace~57c702f9-fed1-4b27-b507-0b0252812bf7/collection/12336342-a5404a90-7d5a-4e9f-a26b-bb2bc93f19a2?action=share&source=collection_link&creator=12336342"
|
||||||
|
},
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "v1",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "organization",
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"name": "register-user",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": "Register a new user or get existing user via organization API key. If user is new or has no organization, they will be assigned to this organization and their wallet will be charged with the organization's user_credit."
|
||||||
|
},
|
||||||
|
"response": [
|
||||||
|
{
|
||||||
|
"name": "success - new user",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": 1,\n \"mobile_number\": \"09123456789\",\n \"organization_id\": 1,\n \"organization_title\": \"Test Organization\",\n \"wallet_balance\": 100.0,\n \"is_new_user\": true,\n \"credit_charged\": 100.0\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "success - existing user",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "OK",
|
||||||
|
"code": 200,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"id\": 1,\n \"mobile_number\": \"09123456789\",\n \"organization_id\": 1,\n \"organization_title\": \"Test Organization\",\n \"wallet_balance\": 100.0,\n \"is_new_user\": false,\n \"credit_charged\": 0.0\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "invalid api key",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "invalid-api-key",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Unauthorized",
|
||||||
|
"code": 401,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"error_code\": \"INVALID_API_KEY\",\n \"message\": \"Invalid API key\",\n \"context\": {\n \"api_key_prefix\": \"invalid-\"\n }\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "organization disabled",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Forbidden",
|
||||||
|
"code": 403,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"error_code\": \"ORGANIZATION_DISABLED\",\n \"message\": \"Organization is disabled\",\n \"context\": {\n \"organization_id\": 1\n }\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "organization expired",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Forbidden",
|
||||||
|
"code": 403,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"error_code\": \"ORGANIZATION_EXPIRED\",\n \"message\": \"Organization has expired\",\n \"context\": {\n \"organization_id\": 1,\n \"expires_at\": \"2024-01-01T00:00:00\"\n }\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "invalid mobile format",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "X-API-Key",
|
||||||
|
"value": "{{organizationApiKey}}",
|
||||||
|
"description": "Organization API Key",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"01234567890\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Unprocessable Entity",
|
||||||
|
"code": 422,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"mobile_number\": \"String should match pattern '^09\\\\d{9}$'\"\n}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "missing api key header",
|
||||||
|
"originalRequest": {
|
||||||
|
"method": "POST",
|
||||||
|
"header": [],
|
||||||
|
"body": {
|
||||||
|
"mode": "raw",
|
||||||
|
"raw": "{\n \"mobile_number\": \"09123456789\"\n}",
|
||||||
|
"options": {
|
||||||
|
"raw": {
|
||||||
|
"language": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"raw": "{{baseURL}}/api/v1/organizations/register-user",
|
||||||
|
"host": [
|
||||||
|
"{{baseURL}}"
|
||||||
|
],
|
||||||
|
"path": [
|
||||||
|
"api",
|
||||||
|
"v1",
|
||||||
|
"organizations",
|
||||||
|
"register-user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"status": "Unprocessable Entity",
|
||||||
|
"code": 422,
|
||||||
|
"_postman_previewlanguage": "json",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "content-type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cookie": [],
|
||||||
|
"body": "{\n \"detail\": [\n {\n \"type\": \"missing\",\n \"loc\": [\"header\", \"x-api-key\"],\n \"msg\": \"Field required\",\n \"input\": null\n }\n ]\n}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"packages": {},
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"packages": {},
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"key": "baseURL",
|
||||||
|
"value": "http://127.0.0.0:8000",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "access_token",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "accessToken",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "refreshToken",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "organizationApiKey",
|
||||||
|
"value": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Commands.UpdateAppVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Command برای آپدیت یا ایجاد نسخه جدید اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public record UpdateAppVersionCommand : IRequest<Unit>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// نام اپلیکیشن (FrontOffice, BackOffice, MobileApp)
|
||||||
|
/// </summary>
|
||||||
|
public string AppName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره نسخه جدید (مثلاً 1.2.3)
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentVersion { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حداقل نسخه مورد نیاز
|
||||||
|
/// </summary>
|
||||||
|
public string? MinRequiredVersion { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا کش کامل باید پاک بشه؟
|
||||||
|
/// </summary>
|
||||||
|
public bool RequiresFullCacheClear { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیام آپدیت
|
||||||
|
/// </summary>
|
||||||
|
public string? UpdateMessage { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// توضیحات تغییرات این نسخه
|
||||||
|
/// </summary>
|
||||||
|
public string? ReleaseNotes { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دلیل آپدیت (برای لاگ)
|
||||||
|
/// </summary>
|
||||||
|
public string? UpdateReason { get; init; }
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
using CMSMicroservice.Domain.Entities.Configuration;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Commands.UpdateAppVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler برای آپدیت یا ایجاد نسخه جدید اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateAppVersionCommandHandler : IRequestHandler<UpdateAppVersionCommand, Unit>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly ILogger<UpdateAppVersionCommandHandler> _logger;
|
||||||
|
|
||||||
|
public UpdateAppVersionCommandHandler(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
ILogger<UpdateAppVersionCommandHandler> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Unit> Handle(UpdateAppVersionCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// پیدا کردن نسخه موجود برای این اپ
|
||||||
|
var existingVersion = await _context.AppVersions
|
||||||
|
.Where(v => v.AppName == request.AppName && !v.IsDeleted)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (existingVersion != null)
|
||||||
|
{
|
||||||
|
// آپدیت نسخه موجود
|
||||||
|
existingVersion.CurrentVersion = request.CurrentVersion;
|
||||||
|
existingVersion.MinRequiredVersion = request.MinRequiredVersion ?? request.CurrentVersion;
|
||||||
|
existingVersion.RequiresFullCacheClear = request.RequiresFullCacheClear;
|
||||||
|
existingVersion.UpdateMessage = request.UpdateMessage;
|
||||||
|
existingVersion.ReleaseNotes = request.ReleaseNotes;
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"App version updated: {AppName} v{Version}, CacheClear={CacheClear}, Reason={Reason}",
|
||||||
|
request.AppName, request.CurrentVersion, request.RequiresFullCacheClear, request.UpdateReason);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// ایجاد نسخه جدید
|
||||||
|
var newVersion = new AppVersion
|
||||||
|
{
|
||||||
|
AppName = request.AppName,
|
||||||
|
CurrentVersion = request.CurrentVersion,
|
||||||
|
MinRequiredVersion = request.MinRequiredVersion ?? request.CurrentVersion,
|
||||||
|
RequiresFullCacheClear = request.RequiresFullCacheClear,
|
||||||
|
UpdateMessage = request.UpdateMessage,
|
||||||
|
ReleaseNotes = request.ReleaseNotes,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.AppVersions.Add(newVersion);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"New app version created: {AppName} v{Version}, CacheClear={CacheClear}, Reason={Reason}",
|
||||||
|
request.AppName, request.CurrentVersion, request.RequiresFullCacheClear, request.UpdateReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
return Unit.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAllAppVersions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت همه نسخههای اپلیکیشنها
|
||||||
|
/// </summary>
|
||||||
|
public record GetAllAppVersionsQuery(bool IncludeInactive = false) : IRequest<List<AppVersionItemDto>>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO برای هر آیتم نسخه اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public record AppVersionItemDto
|
||||||
|
{
|
||||||
|
public long Id { get; init; }
|
||||||
|
public string AppName { get; init; } = string.Empty;
|
||||||
|
public string CurrentVersion { get; init; } = string.Empty;
|
||||||
|
public string MinRequiredVersion { get; init; } = string.Empty;
|
||||||
|
public bool RequiresFullCacheClear { get; init; }
|
||||||
|
public string? UpdateMessage { get; init; }
|
||||||
|
public string? ReleaseNotes { get; init; }
|
||||||
|
public bool IsActive { get; init; }
|
||||||
|
public DateTime Created { get; init; }
|
||||||
|
public DateTime? LastModified { get; init; }
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAllAppVersions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler برای دریافت همه نسخههای اپلیکیشنها
|
||||||
|
/// </summary>
|
||||||
|
public class GetAllAppVersionsQueryHandler : IRequestHandler<GetAllAppVersionsQuery, List<AppVersionItemDto>>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetAllAppVersionsQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<AppVersionItemDto>> Handle(GetAllAppVersionsQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var query = _context.AppVersions
|
||||||
|
.Where(v => !v.IsDeleted);
|
||||||
|
|
||||||
|
if (!request.IncludeInactive)
|
||||||
|
{
|
||||||
|
query = query.Where(v => v.IsActive);
|
||||||
|
}
|
||||||
|
|
||||||
|
var versions = await query
|
||||||
|
.OrderBy(v => v.AppName)
|
||||||
|
.Select(v => new AppVersionItemDto
|
||||||
|
{
|
||||||
|
Id = v.Id,
|
||||||
|
AppName = v.AppName,
|
||||||
|
CurrentVersion = v.CurrentVersion,
|
||||||
|
MinRequiredVersion = v.MinRequiredVersion,
|
||||||
|
RequiresFullCacheClear = v.RequiresFullCacheClear,
|
||||||
|
UpdateMessage = v.UpdateMessage,
|
||||||
|
ReleaseNotes = v.ReleaseNotes,
|
||||||
|
IsActive = v.IsActive,
|
||||||
|
Created = v.Created,
|
||||||
|
LastModified = v.LastModified
|
||||||
|
})
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query برای دریافت آخرین نسخه یک اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public record GetAppVersionQuery(string AppName, string? CurrentClientVersion = null) : IRequest<AppVersionDto?>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO برای اطلاعات نسخه اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public record AppVersionDto
|
||||||
|
{
|
||||||
|
public bool Found { get; init; }
|
||||||
|
public string AppName { get; init; } = string.Empty;
|
||||||
|
public string CurrentVersion { get; init; } = string.Empty;
|
||||||
|
public string MinRequiredVersion { get; init; } = string.Empty;
|
||||||
|
public bool RequiresFullCacheClear { get; init; }
|
||||||
|
public bool RequiresUpdate { get; init; }
|
||||||
|
public string? UpdateMessage { get; init; }
|
||||||
|
public string? ReleaseNotes { get; init; }
|
||||||
|
public DateTime? LastUpdated { get; init; }
|
||||||
|
}
|
||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
using CMSMicroservice.Domain.Entities.Configuration;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handler برای دریافت آخرین نسخه اپلیکیشن
|
||||||
|
/// </summary>
|
||||||
|
public class GetAppVersionQueryHandler : IRequestHandler<GetAppVersionQuery, AppVersionDto?>
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
|
||||||
|
public GetAppVersionQueryHandler(IApplicationDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<AppVersionDto?> Handle(GetAppVersionQuery request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var appVersion = await _context.AppVersions
|
||||||
|
.Where(v => v.AppName == request.AppName && v.IsActive && !v.IsDeleted)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (appVersion == null)
|
||||||
|
{
|
||||||
|
return new AppVersionDto
|
||||||
|
{
|
||||||
|
Found = false,
|
||||||
|
AppName = request.AppName
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// مقایسه ورژن کلاینت با حداقل نسخه مورد نیاز
|
||||||
|
bool requiresUpdate = false;
|
||||||
|
if (!string.IsNullOrEmpty(request.CurrentClientVersion) && !string.IsNullOrEmpty(appVersion.MinRequiredVersion))
|
||||||
|
{
|
||||||
|
requiresUpdate = CompareVersions(request.CurrentClientVersion, appVersion.MinRequiredVersion) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AppVersionDto
|
||||||
|
{
|
||||||
|
Found = true,
|
||||||
|
AppName = appVersion.AppName,
|
||||||
|
CurrentVersion = appVersion.CurrentVersion,
|
||||||
|
MinRequiredVersion = appVersion.MinRequiredVersion,
|
||||||
|
RequiresFullCacheClear = appVersion.RequiresFullCacheClear,
|
||||||
|
RequiresUpdate = requiresUpdate,
|
||||||
|
UpdateMessage = appVersion.UpdateMessage,
|
||||||
|
ReleaseNotes = appVersion.ReleaseNotes,
|
||||||
|
LastUpdated = appVersion.LastModified ?? appVersion.Created
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// مقایسه دو نسخه (مثلاً 1.2.3 با 1.3.0)
|
||||||
|
/// برگشت: منفی = اولی کوچکتر، مثبت = اولی بزرگتر، صفر = برابر
|
||||||
|
/// </summary>
|
||||||
|
private static int CompareVersions(string version1, string version2)
|
||||||
|
{
|
||||||
|
var v1Parts = version1.Split('.').Select(s => int.TryParse(s, out var n) ? n : 0).ToArray();
|
||||||
|
var v2Parts = version2.Split('.').Select(s => int.TryParse(s, out var n) ? n : 0).ToArray();
|
||||||
|
|
||||||
|
var maxLen = Math.Max(v1Parts.Length, v2Parts.Length);
|
||||||
|
|
||||||
|
for (int i = 0; i < maxLen; i++)
|
||||||
|
{
|
||||||
|
var v1 = i < v1Parts.Length ? v1Parts[i] : 0;
|
||||||
|
var v2 = i < v2Parts.Length ? v2Parts[i] : 0;
|
||||||
|
|
||||||
|
if (v1 != v2)
|
||||||
|
return v1.CompareTo(v2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.11" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.4.0" />
|
||||||
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
<PackageReference Include="System.Linq.Dynamic.Core" Version="1.6.10" />
|
||||||
|
|||||||
+178
-12
@@ -1,5 +1,9 @@
|
|||||||
using CMSMicroservice.Application.Common.Exceptions;
|
using CMSMicroservice.Application.Common.Exceptions;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
using CMSMicroservice.Domain.Entities;
|
using CMSMicroservice.Domain.Entities;
|
||||||
|
using CMSMicroservice.Domain.Entities.Club;
|
||||||
|
using CMSMicroservice.Domain.Entities.Commission;
|
||||||
|
using CMSMicroservice.Domain.Entities.History;
|
||||||
using CMSMicroservice.Domain.Enums;
|
using CMSMicroservice.Domain.Enums;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -12,6 +16,8 @@ namespace CMSMicroservice.Application.ClubMembershipCQ.Commands.AcceptClubMember
|
|||||||
/// 1. کد OTP را تایید میکند
|
/// 1. کد OTP را تایید میکند
|
||||||
/// 2. قرارداد را در جدول UserContract ثبت میکند
|
/// 2. قرارداد را در جدول UserContract ثبت میکند
|
||||||
/// 3. باشگاه مشتری را فعال میکند (IsActive = true)
|
/// 3. باشگاه مشتری را فعال میکند (IsActive = true)
|
||||||
|
/// 4. مبلغ را به Pool هفته جاری اضافه میکند
|
||||||
|
/// 5. ویژگیهای باشگاه را به کاربر اعطا میکند
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AcceptClubMembershipContractCommandHandler
|
public class AcceptClubMembershipContractCommandHandler
|
||||||
: IRequestHandler<AcceptClubMembershipContractCommand, AcceptClubMembershipContractResponseDto>
|
: IRequestHandler<AcceptClubMembershipContractCommand, AcceptClubMembershipContractResponseDto>
|
||||||
@@ -19,6 +25,8 @@ public class AcceptClubMembershipContractCommandHandler
|
|||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IConfiguration _cfg;
|
private readonly IConfiguration _cfg;
|
||||||
private readonly IHashService _hashService;
|
private readonly IHashService _hashService;
|
||||||
|
private readonly ICurrentUserService _currentUser;
|
||||||
|
private readonly IWeekDefinitionRepository _weekRepository;
|
||||||
private readonly ILogger<AcceptClubMembershipContractCommandHandler> _logger;
|
private readonly ILogger<AcceptClubMembershipContractCommandHandler> _logger;
|
||||||
|
|
||||||
private const int MaxAttempts = 5;
|
private const int MaxAttempts = 5;
|
||||||
@@ -28,11 +36,15 @@ public class AcceptClubMembershipContractCommandHandler
|
|||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IConfiguration cfg,
|
IConfiguration cfg,
|
||||||
IHashService hashService,
|
IHashService hashService,
|
||||||
|
ICurrentUserService currentUser,
|
||||||
|
IWeekDefinitionRepository weekRepository,
|
||||||
ILogger<AcceptClubMembershipContractCommandHandler> logger)
|
ILogger<AcceptClubMembershipContractCommandHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_cfg = cfg;
|
_cfg = cfg;
|
||||||
_hashService = hashService;
|
_hashService = hashService;
|
||||||
|
_currentUser = currentUser;
|
||||||
|
_weekRepository = weekRepository;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,33 +126,174 @@ public class AcceptClubMembershipContractCommandHandler
|
|||||||
};
|
};
|
||||||
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
await _context.UserContracts.AddAsync(userContract, cancellationToken);
|
||||||
|
|
||||||
// 6. فعالسازی باشگاه مشتریان
|
// 6. دریافت مقادیر از تنظیمات سیستم
|
||||||
if (user.ClubMembership == null)
|
var giftValueConfig = await _context.SystemConfigurations
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
|
c => c.Key == "Club.MembershipGiftValue" && c.IsActive,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
var activationFeeConfig = await _context.SystemConfigurations
|
||||||
|
.FirstOrDefaultAsync(
|
||||||
|
c => c.Key == "Club.ActivationFee" && c.IsActive,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
long giftValue = 28_000_000; // مقدار پیشفرض
|
||||||
|
if (giftValueConfig != null && long.TryParse(giftValueConfig.Value, out var configValue))
|
||||||
{
|
{
|
||||||
user.ClubMembership = new Domain.Entities.Club.ClubMembership
|
giftValue = configValue;
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Using Club.MembershipGiftValue from configuration: {GiftValue}",
|
||||||
|
giftValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
long activationFeeValue = 25_200_000; // مقدار پیشفرض
|
||||||
|
if (activationFeeConfig != null && long.TryParse(activationFeeConfig.Value, out var activationFeeConfigValue))
|
||||||
|
{
|
||||||
|
activationFeeValue = activationFeeConfigValue;
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Using Club.ActivationFee from configuration: {activationFeeValue}",
|
||||||
|
activationFeeValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. فعالسازی باشگاه مشتریان
|
||||||
|
ClubMembership clubMembership;
|
||||||
|
bool isNewMembership = user.ClubMembership == null;
|
||||||
|
var activationDate = DateTime.Now;
|
||||||
|
|
||||||
|
if (isNewMembership)
|
||||||
|
{
|
||||||
|
clubMembership = new ClubMembership
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
UserId = user.Id,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
ActivatedAt = DateTime.Now,
|
ActivatedAt = activationDate,
|
||||||
InitialContribution = 56_000_000,
|
InitialContribution = activationFeeValue,
|
||||||
GiftValue = 25_200_000,
|
GiftValue = giftValue,
|
||||||
|
TotalEarned = 0,
|
||||||
PurchaseMethod = user.PackagePurchaseMethod
|
PurchaseMethod = user.PackagePurchaseMethod
|
||||||
};
|
};
|
||||||
await _context.ClubMemberships.AddAsync(user.ClubMembership, cancellationToken);
|
await _context.ClubMemberships.AddAsync(clubMembership, cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Created new club membership for UserId {UserId} via {Method}, GiftValue: {GiftValue}",
|
||||||
|
user.Id,
|
||||||
|
user.PackagePurchaseMethod,
|
||||||
|
giftValue
|
||||||
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
user.ClubMembership.IsActive = true;
|
clubMembership = user.ClubMembership!;
|
||||||
user.ClubMembership.ActivatedAt = DateTime.Now;
|
clubMembership.IsActive = true;
|
||||||
_context.ClubMemberships.Update(user.ClubMembership);
|
clubMembership.ActivatedAt = activationDate;
|
||||||
|
clubMembership.PurchaseMethod = user.PackagePurchaseMethod;
|
||||||
|
_context.ClubMemberships.Update(clubMembership);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Reactivated club membership for UserId {UserId}",
|
||||||
|
user.Id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 8. ثبت تاریخچه
|
||||||
|
var history = new ClubMembershipHistory
|
||||||
|
{
|
||||||
|
ClubMembershipId = clubMembership.Id,
|
||||||
|
UserId = clubMembership.UserId,
|
||||||
|
OldIsActive = !isNewMembership && !user.ClubMembership!.IsActive,
|
||||||
|
NewIsActive = true,
|
||||||
|
Action = ClubMembershipAction.Activated,
|
||||||
|
Reason = isNewMembership
|
||||||
|
? $"Initial activation via contract signing - {user.PackagePurchaseMethod}"
|
||||||
|
: $"Reactivated via contract signing - {user.PackagePurchaseMethod}",
|
||||||
|
PerformedBy = _currentUser.GetPerformedBy()
|
||||||
|
};
|
||||||
|
|
||||||
|
_context.ClubMembershipHistories.Add(history);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 9. اضافه کردن مبلغ به Pool هفته جاری
|
||||||
|
var currentWeekDefinitionId = GetCurrentWeekDefinitionId();
|
||||||
|
var weeklyPool = await _context.WeeklyCommissionPools
|
||||||
|
.FirstOrDefaultAsync(p => p.WeekDefinitionId == currentWeekDefinitionId, cancellationToken);
|
||||||
|
|
||||||
|
if (weeklyPool == null)
|
||||||
|
{
|
||||||
|
weeklyPool = new WeeklyCommissionPool
|
||||||
|
{
|
||||||
|
WeekDefinitionId = currentWeekDefinitionId,
|
||||||
|
TotalPoolAmount = activationFeeValue,
|
||||||
|
TotalBalances = 0,
|
||||||
|
ValuePerBalance = 0,
|
||||||
|
IsCalculated = false,
|
||||||
|
CalculatedAt = null
|
||||||
|
};
|
||||||
|
|
||||||
|
await _context.WeeklyCommissionPools.AddAsync(weeklyPool, cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Created new WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId} with initial amount: {Amount}",
|
||||||
|
currentWeekDefinitionId,
|
||||||
|
activationFeeValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
weeklyPool.TotalPoolAmount += activationFeeValue;
|
||||||
|
_context.WeeklyCommissionPools.Update(weeklyPool);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Added {Amount} to existing WeeklyCommissionPool for WeekDefinitionId={WeekDefinitionId}. New total: {NewTotal}",
|
||||||
|
activationFeeValue,
|
||||||
|
currentWeekDefinitionId,
|
||||||
|
weeklyPool.TotalPoolAmount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 10. اعطای ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
||||||
|
if (isNewMembership)
|
||||||
|
{
|
||||||
|
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||||
|
var clubFeatures = await _context.ClubFeatures
|
||||||
|
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (clubFeatures.Any())
|
||||||
|
{
|
||||||
|
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
ClubMembershipId = clubMembership.Id,
|
||||||
|
ClubFeatureId = feature.Id,
|
||||||
|
GrantedAt = activationDate,
|
||||||
|
IsActive = true,
|
||||||
|
Notes = "اعطا شده هنگام امضای قرارداد"
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
_context.UserClubFeatures.AddRange(userClubFeatures);
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Granted {Count} club features to UserId {UserId}",
|
||||||
|
clubFeatures.Count,
|
||||||
|
user.Id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_logger.LogInformation(
|
_logger.LogInformation(
|
||||||
"Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}",
|
"Club membership contract accepted and activated for UserId: {UserId}, ContractId: {ContractId}, MembershipId: {MembershipId}",
|
||||||
request.UserId,
|
request.UserId,
|
||||||
userContract.Id
|
userContract.Id,
|
||||||
|
clubMembership.Id
|
||||||
);
|
);
|
||||||
|
|
||||||
return new AcceptClubMembershipContractResponseDto
|
return new AcceptClubMembershipContractResponseDto
|
||||||
@@ -151,6 +304,19 @@ public class AcceptClubMembershipContractCommandHandler
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت شناسه تعریف هفته جاری
|
||||||
|
/// </summary>
|
||||||
|
private long GetCurrentWeekDefinitionId()
|
||||||
|
{
|
||||||
|
var week = _weekRepository.GetCurrentWeek();
|
||||||
|
if (week == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("هفته جاری در سیستم تعریف نشده است");
|
||||||
|
}
|
||||||
|
return week.Id;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<(bool Success, string Message)> VerifyOtpAsync(
|
private async Task<(bool Success, string Message)> VerifyOtpAsync(
|
||||||
string mobile,
|
string mobile,
|
||||||
string code,
|
string code,
|
||||||
|
|||||||
+2
-1
@@ -297,8 +297,9 @@ public class ActivateClubMembershipCommandHandler : IRequestHandler<ActivateClub
|
|||||||
// 9. اضافه کردن ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
// 9. اضافه کردن ویژگیهای باشگاه برای کاربر (فقط برای عضویت جدید)
|
||||||
if (isNewMembership)
|
if (isNewMembership)
|
||||||
{
|
{
|
||||||
|
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||||||
var clubFeatures = await _context.ClubFeatures
|
var clubFeatures = await _context.ClubFeatures
|
||||||
.Where(f => !f.IsDeleted && new long[] { 1, 2, 3, 4 }.Contains(f.Id))
|
.Where(f => !f.IsDeleted && featureIds.Contains(f.Id))
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
if (clubFeatures.Any())
|
if (clubFeatures.Any())
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using CMSMicroservice.Domain.Entities.Payment;
|
|||||||
using CMSMicroservice.Domain.Entities.Order;
|
using CMSMicroservice.Domain.Entities.Order;
|
||||||
using CMSMicroservice.Domain.Entities.DiscountShop;
|
using CMSMicroservice.Domain.Entities.DiscountShop;
|
||||||
using CMSMicroservice.Domain.Entities.Geography;
|
using CMSMicroservice.Domain.Entities.Geography;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.Common.Interfaces;
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ public interface IApplicationDbContext
|
|||||||
DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; }
|
DbSet<CommissionPayoutHistory> CommissionPayoutHistories { get; }
|
||||||
DbSet<WorkerExecutionLog> WorkerExecutionLogs { get; }
|
DbSet<WorkerExecutionLog> WorkerExecutionLogs { get; }
|
||||||
DbSet<DayaLoanContract> DayaLoanContracts { get; }
|
DbSet<DayaLoanContract> DayaLoanContracts { get; }
|
||||||
|
DbSet<AppVersion> AppVersions { get; }
|
||||||
|
|
||||||
// ============= Discount Shop =============
|
// ============= Discount Shop =============
|
||||||
DbSet<DiscountProduct> DiscountProducts { get; }
|
DbSet<DiscountProduct> DiscountProducts { get; }
|
||||||
@@ -60,5 +62,10 @@ public interface IApplicationDbContext
|
|||||||
DbSet<State> States { get; }
|
DbSet<State> States { get; }
|
||||||
DbSet<City> Cities { get; }
|
DbSet<City> Cities { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures
|
||||||
|
/// </summary>
|
||||||
|
DatabaseFacade Database { get; }
|
||||||
|
|
||||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
namespace CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// سرویس ارتباط با API چتیکا برای ساخت حساب کاربری
|
||||||
|
/// </summary>
|
||||||
|
public interface IChatikaApiService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد حساب کاربری در چتیکا
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="mobileNumber">شماره موبایل کاربر</param>
|
||||||
|
/// <param name="fullName">نام کامل کاربر</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>نتیجه ایجاد حساب</returns>
|
||||||
|
Task<ChatikaAccountResult> CreateAccountAsync(
|
||||||
|
string mobileNumber,
|
||||||
|
string fullName,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نتیجه ایجاد حساب در چتیکا
|
||||||
|
/// </summary>
|
||||||
|
public class ChatikaAccountResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// موفقیت عملیات
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیام خطا در صورت عدم موفقیت
|
||||||
|
/// </summary>
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شناسه کاربر در چتیکا
|
||||||
|
/// </summary>
|
||||||
|
public string? ChatikaUserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// URL دسترسی به چتیکا
|
||||||
|
/// </summary>
|
||||||
|
public string? AccessUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد نتیجه موفق
|
||||||
|
/// </summary>
|
||||||
|
public static ChatikaAccountResult Success(string? chatikaUserId = null, string? accessUrl = null) => new()
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
ChatikaUserId = chatikaUserId,
|
||||||
|
AccessUrl = accessUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ایجاد نتیجه ناموفق
|
||||||
|
/// </summary>
|
||||||
|
public static ChatikaAccountResult Failure(string errorMessage) => new()
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
ErrorMessage = errorMessage
|
||||||
|
};
|
||||||
|
}
|
||||||
+149
-113
@@ -1,144 +1,180 @@
|
|||||||
|
using System.Data;
|
||||||
|
using System.Data.Common;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|
||||||
public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, NetworkTreeDto?>
|
public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, NetworkTreeDto?>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _context;
|
private readonly IApplicationDbContext _context;
|
||||||
private readonly IWeekDefinitionRepository _weekDefinitionRepository;
|
private readonly ILogger<GetNetworkTreeQueryHandler> _logger;
|
||||||
|
|
||||||
public GetNetworkTreeQueryHandler(
|
public GetNetworkTreeQueryHandler(
|
||||||
IApplicationDbContext context,
|
IApplicationDbContext context,
|
||||||
IWeekDefinitionRepository weekDefinitionRepository)
|
ILogger<GetNetworkTreeQueryHandler> logger)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_weekDefinitionRepository = weekDefinitionRepository;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
public async Task<NetworkTreeDto?> Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var rootUser = await _context.Users
|
try
|
||||||
.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken);
|
|
||||||
|
|
||||||
if (rootUser == null)
|
|
||||||
{
|
{
|
||||||
return null;
|
// دریافت نتایج flat از Stored Procedure
|
||||||
}
|
var flatNodes = await ExecuteStoredProcedureAsync(request, cancellationToken);
|
||||||
|
|
||||||
var tree = await BuildTree(rootUser.Id, request.MaxDepth, 0, cancellationToken, request);
|
if (flatNodes == null || !flatNodes.Any())
|
||||||
return tree;
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// تبدیل نتایج flat به ساختار درختی
|
||||||
|
var tree = BuildTreeFromFlatNodes(flatNodes);
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error executing GetNetworkTree for UserId: {UserId}", request.UserId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<NetworkTreeDto> BuildTree(long userId, int maxDepth, int currentDepth, CancellationToken cancellationToken, GetNetworkTreeQuery request)
|
/// <summary>
|
||||||
|
/// اجرای Stored Procedure و دریافت نتایج
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<NetworkTreeNodeDto>> ExecuteStoredProcedureAsync(
|
||||||
|
GetNetworkTreeQuery request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// دریافت کاربر با اطلاعات باشگاه مشتریان
|
var results = new List<NetworkTreeNodeDto>();
|
||||||
var user = await _context.Users
|
|
||||||
.Include(u => u.ClubMembership)
|
|
||||||
.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
|
||||||
|
|
||||||
if (user == null)
|
var connection = _context.Database.GetDbConnection();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
throw new NotFoundException(nameof(User), userId);
|
if (connection.State != ConnectionState.Open)
|
||||||
|
{
|
||||||
|
await connection.OpenAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = "[CMS].[GetNetworkTree]";
|
||||||
|
command.CommandType = CommandType.StoredProcedure;
|
||||||
|
command.CommandTimeout = 120; // 2 minutes timeout for large trees
|
||||||
|
|
||||||
|
// Parameters - use DbParameter for provider-agnostic code
|
||||||
|
var rootUserIdParam = command.CreateParameter();
|
||||||
|
rootUserIdParam.ParameterName = "@RootUserId";
|
||||||
|
rootUserIdParam.DbType = DbType.Int64;
|
||||||
|
rootUserIdParam.Value = request.UserId;
|
||||||
|
command.Parameters.Add(rootUserIdParam);
|
||||||
|
|
||||||
|
var maxDepthParam = command.CreateParameter();
|
||||||
|
maxDepthParam.ParameterName = "@MaxDepth";
|
||||||
|
maxDepthParam.DbType = DbType.Int32;
|
||||||
|
maxDepthParam.Value = request.MaxDepth > 0 ? request.MaxDepth : 100;
|
||||||
|
command.Parameters.Add(maxDepthParam);
|
||||||
|
|
||||||
|
var isClubActiveParam = command.CreateParameter();
|
||||||
|
isClubActiveParam.ParameterName = "@IsClubActive";
|
||||||
|
isClubActiveParam.DbType = DbType.Boolean;
|
||||||
|
isClubActiveParam.Value = request.IsClubActive.HasValue ? request.IsClubActive.Value : DBNull.Value;
|
||||||
|
command.Parameters.Add(isClubActiveParam);
|
||||||
|
|
||||||
|
var weekParam = command.CreateParameter();
|
||||||
|
weekParam.ParameterName = "@ActivationWeekDefinitionId";
|
||||||
|
weekParam.DbType = DbType.Int64;
|
||||||
|
weekParam.Value = request.ActivationWeekDefinitionId.HasValue ? request.ActivationWeekDefinitionId.Value : DBNull.Value;
|
||||||
|
command.Parameters.Add(weekParam);
|
||||||
|
|
||||||
|
using var reader = await command.ExecuteReaderAsync(cancellationToken);
|
||||||
|
|
||||||
|
while (await reader.ReadAsync(cancellationToken))
|
||||||
|
{
|
||||||
|
var node = new NetworkTreeNodeDto
|
||||||
|
{
|
||||||
|
UserId = reader.GetInt64(reader.GetOrdinal("UserId")),
|
||||||
|
Mobile = reader.IsDBNull(reader.GetOrdinal("Mobile")) ? null : reader.GetString(reader.GetOrdinal("Mobile")),
|
||||||
|
FirstName = reader.IsDBNull(reader.GetOrdinal("FirstName")) ? null : reader.GetString(reader.GetOrdinal("FirstName")),
|
||||||
|
LastName = reader.IsDBNull(reader.GetOrdinal("LastName")) ? null : reader.GetString(reader.GetOrdinal("LastName")),
|
||||||
|
ReferralCode = reader.IsDBNull(reader.GetOrdinal("ReferralCode")) ? null : reader.GetString(reader.GetOrdinal("ReferralCode")),
|
||||||
|
LegPosition = reader.IsDBNull(reader.GetOrdinal("LegPosition")) ? null : reader.GetInt32(reader.GetOrdinal("LegPosition")),
|
||||||
|
ParentId = reader.IsDBNull(reader.GetOrdinal("ParentId")) ? null : reader.GetInt64(reader.GetOrdinal("ParentId")),
|
||||||
|
NetworkLevel = reader.GetInt32(reader.GetOrdinal("NetworkLevel")),
|
||||||
|
ClubActivatedAt = reader.IsDBNull(reader.GetOrdinal("ClubActivatedAt")) ? null : reader.GetDateTime(reader.GetOrdinal("ClubActivatedAt")),
|
||||||
|
IsClubActive = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsClubActive"))),
|
||||||
|
ActivationWeekDefinitionId = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDefinitionId")) ? null : reader.GetInt64(reader.GetOrdinal("ActivationWeekDefinitionId")),
|
||||||
|
ActivationWeekDisplayName = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDisplayName")) ? null : reader.GetString(reader.GetOrdinal("ActivationWeekDisplayName")),
|
||||||
|
IsActivatedInTargetWeek = Convert.ToBoolean(reader.GetValue(reader.GetOrdinal("IsActivatedInTargetWeek"))),
|
||||||
|
UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated")))
|
||||||
|
};
|
||||||
|
|
||||||
|
results.Add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Connection is managed by DbContext, don't close it here
|
||||||
}
|
}
|
||||||
|
|
||||||
// محاسبه شماره هفته فعالسازی
|
_logger.LogInformation("GetNetworkTree SP returned {Count} nodes for UserId: {UserId}", results.Count, request.UserId);
|
||||||
long? activationWeekDefinitionId = null;
|
return results;
|
||||||
string? activationWeekDisplayName = null;
|
}
|
||||||
bool isActivatedInTargetWeek = false;
|
|
||||||
|
|
||||||
if (user.ClubMembership?.ActivatedAt != null)
|
/// <summary>
|
||||||
|
/// تبدیل لیست flat به ساختار درختی
|
||||||
|
/// </summary>
|
||||||
|
private NetworkTreeDto? BuildTreeFromFlatNodes(List<NetworkTreeNodeDto> flatNodes)
|
||||||
|
{
|
||||||
|
if (!flatNodes.Any()) return null;
|
||||||
|
|
||||||
|
// Dictionary برای دسترسی سریع به نودها
|
||||||
|
var nodeDict = new Dictionary<long, NetworkTreeDto>();
|
||||||
|
|
||||||
|
// ایجاد همه نودها
|
||||||
|
foreach (var flatNode in flatNodes)
|
||||||
{
|
{
|
||||||
// activationWeekDefinitionId = CalculateWeekNumber(user.ClubMembership.ActivatedAt.Value);
|
nodeDict[flatNode.UserId] = new NetworkTreeDto
|
||||||
var week = _weekDefinitionRepository.GetWeekByDate(user.ClubMembership.ActivatedAt.Value);
|
|
||||||
activationWeekDefinitionId = week.Id;
|
|
||||||
activationWeekDisplayName = week.DisplayName;
|
|
||||||
|
|
||||||
// بررسی آیا در هفته هدف فعال شده است
|
|
||||||
if (request.ActivationWeekDefinitionId!=null)
|
|
||||||
{
|
{
|
||||||
isActivatedInTargetWeek = activationWeekDefinitionId == request.ActivationWeekDefinitionId;
|
UserId = flatNode.UserId,
|
||||||
|
Mobile = flatNode.Mobile,
|
||||||
|
FirstName = flatNode.FirstName,
|
||||||
|
LastName = flatNode.LastName,
|
||||||
|
ReferralCode = flatNode.ReferralCode,
|
||||||
|
LegPosition = flatNode.LegPosition.HasValue ? (NetworkLeg)flatNode.LegPosition.Value : null,
|
||||||
|
CurrentDepth = flatNode.NetworkLevel,
|
||||||
|
ClubActivatedAt = flatNode.ClubActivatedAt,
|
||||||
|
IsClubActive = flatNode.IsClubActive,
|
||||||
|
ActivationWeekDefinitionId = flatNode.ActivationWeekDefinitionId,
|
||||||
|
ActivationWeekDisplayName = flatNode.ActivationWeekDisplayName,
|
||||||
|
IsActivatedInTargetWeek = flatNode.IsActivatedInTargetWeek,
|
||||||
|
UserCreated = flatNode.UserCreated
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// برقراری ارتباط Parent-Child
|
||||||
|
foreach (var flatNode in flatNodes)
|
||||||
|
{
|
||||||
|
if (flatNode.ParentId.HasValue && nodeDict.ContainsKey(flatNode.ParentId.Value))
|
||||||
|
{
|
||||||
|
var parent = nodeDict[flatNode.ParentId.Value];
|
||||||
|
var child = nodeDict[flatNode.UserId];
|
||||||
|
|
||||||
|
// تعیین موقعیت چپ یا راست
|
||||||
|
if (flatNode.LegPosition == (int)NetworkLeg.Left)
|
||||||
|
{
|
||||||
|
parent.LeftChild = child;
|
||||||
|
}
|
||||||
|
else if (flatNode.LegPosition == (int)NetworkLeg.Right)
|
||||||
|
{
|
||||||
|
parent.RightChild = child;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var node = new NetworkTreeDto
|
// پیدا کردن ریشه (اولین نود با Level=0)
|
||||||
{
|
var rootNode = flatNodes.FirstOrDefault(n => n.NetworkLevel == 0);
|
||||||
UserId = user.Id,
|
return rootNode != null ? nodeDict[rootNode.UserId] : null;
|
||||||
Mobile = user.Mobile,
|
|
||||||
FirstName = user.FirstName,
|
|
||||||
LastName = user.LastName,
|
|
||||||
LegPosition = user.LegPosition,
|
|
||||||
CurrentDepth = currentDepth,
|
|
||||||
ClubActivatedAt = user.ClubMembership?.ActivatedAt,
|
|
||||||
IsClubActive = user.ClubMembership?.IsActive ?? false,
|
|
||||||
ActivationWeekDefinitionId = activationWeekDefinitionId,
|
|
||||||
ActivationWeekDisplayName = activationWeekDisplayName,
|
|
||||||
IsActivatedInTargetWeek = isActivatedInTargetWeek,
|
|
||||||
UserCreated = user.Created
|
|
||||||
};
|
|
||||||
|
|
||||||
// اگر به حداکثر عمق رسیدیم، دیگر فرزندان را نمیخوانیم
|
|
||||||
if (currentDepth >= maxDepth)
|
|
||||||
{
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
|
|
||||||
// پیدا کردن فرزندان (چپ و راست)
|
|
||||||
var children = await GetFilteredChildren(userId, request, cancellationToken);
|
|
||||||
|
|
||||||
var leftChild = children.FirstOrDefault(c => c.LegPosition == NetworkLeg.Left);
|
|
||||||
if (leftChild != null)
|
|
||||||
{
|
|
||||||
node.LeftChild = await BuildTree(leftChild.Id, maxDepth, currentDepth + 1, cancellationToken, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
var rightChild = children.FirstOrDefault(c => c.LegPosition == NetworkLeg.Right);
|
|
||||||
if (rightChild != null)
|
|
||||||
{
|
|
||||||
node.RightChild = await BuildTree(rightChild.Id, maxDepth, currentDepth + 1, cancellationToken, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// دریافت فرزندان با اعمال فیلترها
|
|
||||||
/// </summary>
|
|
||||||
private async Task<List<User>> GetFilteredChildren(long parentId, GetNetworkTreeQuery request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var query = _context.Users
|
|
||||||
.Include(u => u.ClubMembership)
|
|
||||||
.AsNoTracking()
|
|
||||||
.Where(x => x.NetworkParentId == parentId);
|
|
||||||
|
|
||||||
// اعمال فیلتر IsClubActive
|
|
||||||
if (request.IsClubActive.HasValue)
|
|
||||||
{
|
|
||||||
query = query.Where(u =>
|
|
||||||
u.ClubMembership != null &&
|
|
||||||
u.ClubMembership.IsDeleted == false &&
|
|
||||||
u.ClubMembership.IsActive == request.IsClubActive.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await query.ToListAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// محاسبه شماره هفته از تاریخ
|
|
||||||
/// </summary>
|
|
||||||
// private long CalculateWeekNumber(DateTime date)
|
|
||||||
// {
|
|
||||||
// // First try to get from repository cache
|
|
||||||
// var weekDef = _weekDefinitionRepository.GetWeekByDate(date);
|
|
||||||
// if (weekDef != null)
|
|
||||||
// {
|
|
||||||
// return weekDef.Id;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Fallback: use repository's calculation method
|
|
||||||
// // return _weekDefinitionRepository.CalculateGregorianWeekNumber(date);
|
|
||||||
// return 0;
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -9,8 +9,8 @@ public class GetNetworkTreeQueryValidator : AbstractValidator<GetNetworkTreeQuer
|
|||||||
.WithMessage("شناسه کاربر معتبر نیست");
|
.WithMessage("شناسه کاربر معتبر نیست");
|
||||||
|
|
||||||
RuleFor(x => x.MaxDepth)
|
RuleFor(x => x.MaxDepth)
|
||||||
.InclusiveBetween(1, 20)
|
.InclusiveBetween(1, 100)
|
||||||
.WithMessage("عمق درخت باید بین 1 تا 20 باشد");
|
.WithMessage("عمق درخت باید بین 1 تا 100 باشد");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
|
||||||
|
|||||||
+6
@@ -9,6 +9,12 @@ public class NetworkTreeDto
|
|||||||
public string? Mobile { get; set; }
|
public string? Mobile { get; set; }
|
||||||
public string? FirstName { get; set; }
|
public string? FirstName { get; set; }
|
||||||
public string? LastName { get; set; }
|
public string? LastName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// کد معرف کاربر
|
||||||
|
/// </summary>
|
||||||
|
public string? ReferralCode { get; set; }
|
||||||
|
|
||||||
public NetworkLeg? LegPosition { get; set; }
|
public NetworkLeg? LegPosition { get; set; }
|
||||||
public int CurrentDepth { get; set; }
|
public int CurrentDepth { get; set; }
|
||||||
|
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DTO برای نتیجه Flat از Stored Procedure
|
||||||
|
/// هر ردیف یک نود از درخت است
|
||||||
|
/// </summary>
|
||||||
|
public class NetworkTreeNodeDto
|
||||||
|
{
|
||||||
|
public long UserId { get; set; }
|
||||||
|
public string? Mobile { get; set; }
|
||||||
|
public string? FirstName { get; set; }
|
||||||
|
public string? LastName { get; set; }
|
||||||
|
public string? ReferralCode { get; set; }
|
||||||
|
public int? LegPosition { get; set; }
|
||||||
|
public long? ParentId { get; set; }
|
||||||
|
public int NetworkLevel { get; set; }
|
||||||
|
public DateTime? ClubActivatedAt { get; set; }
|
||||||
|
public bool IsClubActive { get; set; }
|
||||||
|
public long? ActivationWeekDefinitionId { get; set; }
|
||||||
|
public string? ActivationWeekDisplayName { get; set; }
|
||||||
|
public bool IsActivatedInTargetWeek { get; set; }
|
||||||
|
public DateTimeOffset UserCreated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نام کامل کاربر
|
||||||
|
/// </summary>
|
||||||
|
public string FullName => $"{FirstName} {LastName}".Trim();
|
||||||
|
}
|
||||||
+23
-1
@@ -1,3 +1,4 @@
|
|||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
using CMSMicroservice.Domain.Events;
|
using CMSMicroservice.Domain.Events;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
@@ -75,6 +76,26 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
|||||||
if (await _context.Users.CountAsync(x => x.NetworkParentId == parent.Id, cancellationToken: cancellationToken) > 1)
|
if (await _context.Users.CountAsync(x => x.NetworkParentId == parent.Id, cancellationToken: cancellationToken) > 1)
|
||||||
return new VerifyOtpTokenResponseDto() { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
|
return new VerifyOtpTokenResponseDto() { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
|
||||||
|
|
||||||
|
// تعیین موقعیت در شبکه (چپ یا راست)
|
||||||
|
var existingChildren = await _context.Users
|
||||||
|
.Where(x => x.NetworkParentId == parent.Id && !x.IsDeleted)
|
||||||
|
.Select(x => x.LegPosition)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
NetworkLeg newUserLegPosition;
|
||||||
|
if (!existingChildren.Any(x => x == NetworkLeg.Left))
|
||||||
|
{
|
||||||
|
newUserLegPosition = NetworkLeg.Left;
|
||||||
|
}
|
||||||
|
else if (!existingChildren.Any(x => x == NetworkLeg.Right))
|
||||||
|
{
|
||||||
|
newUserLegPosition = NetworkLeg.Right;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new VerifyOtpTokenResponseDto() { Success = false, Message = "ظرفیت معرف تکمیل است!!" };
|
||||||
|
}
|
||||||
|
|
||||||
user = new User
|
user = new User
|
||||||
{
|
{
|
||||||
Mobile = mobile,
|
Mobile = mobile,
|
||||||
@@ -83,7 +104,8 @@ public class VerifyOtpTokenCommandHandler : IRequestHandler<VerifyOtpTokenComman
|
|||||||
MobileVerifiedAt = now,
|
MobileVerifiedAt = now,
|
||||||
IsRulesAccepted = true,
|
IsRulesAccepted = true,
|
||||||
RulesAcceptedAt = now,
|
RulesAcceptedAt = now,
|
||||||
NetworkParentId = parent.Id
|
NetworkParentId = parent.Id,
|
||||||
|
LegPosition = newUserLegPosition
|
||||||
};
|
};
|
||||||
await _context.Users.AddAsync(user, cancellationToken);
|
await _context.Users.AddAsync(user, cancellationToken);
|
||||||
user.AddDomainEvent(new CreateNewUserEvent(user));
|
user.AddDomainEvent(new CreateNewUserEvent(user));
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
namespace CMSMicroservice.Domain.Entities.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// نسخه اپلیکیشنهای فرانتاند
|
||||||
|
/// وقتی ورژن آپدیت بشه، فرانتها باید کش و دادههای محلی رو پاک کنن
|
||||||
|
/// </summary>
|
||||||
|
public class AppVersion : BaseAuditableEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// نام اپلیکیشن (FrontOffice, BackOffice, MobileApp)
|
||||||
|
/// </summary>
|
||||||
|
public string AppName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// شماره نسخه فعلی (مثلاً 1.2.3)
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentVersion { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// حداقل نسخه مورد نیاز - اگر کاربر از این پایینتر باشه باید آپدیت کنه
|
||||||
|
/// </summary>
|
||||||
|
public string MinRequiredVersion { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// آیا کش کامل باید پاک بشه؟
|
||||||
|
/// </summary>
|
||||||
|
public bool RequiresFullCacheClear { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیام آپدیت برای نمایش به کاربر
|
||||||
|
/// </summary>
|
||||||
|
public string? UpdateMessage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// توضیحات تغییرات این نسخه
|
||||||
|
/// </summary>
|
||||||
|
public string? ReleaseNotes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// فعال یا غیرفعال
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
namespace CMSMicroservice.Domain.Enums;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// انواع ویژگیهای باشگاه مشتریان
|
||||||
|
/// </summary>
|
||||||
|
public enum ClubFeatureType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// چتیکا - دستیار هوش مصنوعی
|
||||||
|
/// </summary>
|
||||||
|
Chatika = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// بیمه - خدمات بیمهای
|
||||||
|
/// </summary>
|
||||||
|
Bime = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// تریپ - خدمات سفر و گردشگری
|
||||||
|
/// </summary>
|
||||||
|
Trip = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// لرن - آموزش و یادگیری
|
||||||
|
/// </summary>
|
||||||
|
Learn = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods برای ClubFeatureType
|
||||||
|
/// </summary>
|
||||||
|
public static class ClubFeatureTypeExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت تمام مقادیر ClubFeatureType به صورت آرایه long
|
||||||
|
/// </summary>
|
||||||
|
public static long[] GetAllFeatureIds()
|
||||||
|
{
|
||||||
|
return Enum.GetValues<ClubFeatureType>()
|
||||||
|
.Select(f => (long)f)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// دریافت عنوان فارسی ویژگی
|
||||||
|
/// </summary>
|
||||||
|
public static string GetPersianTitle(this ClubFeatureType featureType)
|
||||||
|
{
|
||||||
|
return featureType switch
|
||||||
|
{
|
||||||
|
ClubFeatureType.Chatika => "چتیکا",
|
||||||
|
ClubFeatureType.Bime => "بیمه",
|
||||||
|
ClubFeatureType.Trip => "تور و سفر",
|
||||||
|
ClubFeatureType.Learn => "آموزش",
|
||||||
|
_ => featureType.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using CMSMicroservice.Domain.Entities.Club;
|
||||||
|
using CMSMicroservice.Domain.Enums;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Polly;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.BackgroundJobs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hangfire Job برای فعالسازی حساب چتیکا برای اعضای جدید باشگاه
|
||||||
|
/// این Job کاربرانی که باشگاهشان فعال شده ولی حساب چتیکا ندارند را پیدا کرده و حساب میسازد
|
||||||
|
/// </summary>
|
||||||
|
public class ChatikaAccountActivationJob
|
||||||
|
{
|
||||||
|
private readonly IApplicationDbContext _context;
|
||||||
|
private readonly IChatikaApiService _chatikaApiService;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
|
private readonly ILogger<ChatikaAccountActivationJob> _logger;
|
||||||
|
private readonly ResiliencePipeline _retryPipeline;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// توضیحات فارسی فیچر چتیکا
|
||||||
|
/// </summary>
|
||||||
|
private const string ChatikaFeatureDescription =
|
||||||
|
"🎉 تبریک! حساب هوش مصنوعی چتیکا شما فعال شد.\n\n" +
|
||||||
|
"برای استفاده از امکانات رایگان چتیکا:\n" +
|
||||||
|
"1️⃣ به وبسایت chatika.ir مراجعه کنید\n" +
|
||||||
|
"2️⃣ شماره موبایل خود را وارد کنید\n" +
|
||||||
|
"3️⃣ از دستیار هوشمند چتیکا لذت ببرید!\n\n" +
|
||||||
|
"🔗 لینک ورود: https://chatika.ir";
|
||||||
|
|
||||||
|
public ChatikaAccountActivationJob(
|
||||||
|
IApplicationDbContext context,
|
||||||
|
IChatikaApiService chatikaApiService,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<ChatikaAccountActivationJob> logger)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
_chatikaApiService = chatikaApiService;
|
||||||
|
_configuration = configuration;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Polly Retry: 3 تلاش با فاصله نمایی
|
||||||
|
_retryPipeline = new ResiliencePipelineBuilder()
|
||||||
|
.AddRetry(new Polly.Retry.RetryStrategyOptions
|
||||||
|
{
|
||||||
|
MaxRetryAttempts = 3,
|
||||||
|
Delay = TimeSpan.FromSeconds(30),
|
||||||
|
BackoffType = Polly.DelayBackoffType.Exponential,
|
||||||
|
UseJitter = true,
|
||||||
|
OnRetry = args =>
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"⚠️ Retry attempt {AttemptNumber} for Chatika API. Exception: {ExceptionType}",
|
||||||
|
args.AttemptNumber,
|
||||||
|
args.Outcome.Exception?.GetType().Name ?? "None");
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// اجرای Job برای فعالسازی حساب چتیکا
|
||||||
|
/// </summary>
|
||||||
|
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// Check if Chatika integration is enabled
|
||||||
|
var enabled = _configuration.GetValue<bool>("Chatika:Enabled");
|
||||||
|
if (!enabled)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Chatika integration is disabled (Chatika:Enabled = false). Skipping job.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var executionId = Guid.NewGuid();
|
||||||
|
_logger.LogInformation(
|
||||||
|
"🚀 [{ExecutionId}] Starting Chatika account activation job",
|
||||||
|
executionId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// پیدا کردن کاربرانی که:
|
||||||
|
// 1. باشگاه فعال دارند (ClubMembership.IsActive = true)
|
||||||
|
// 2. فیچر چتیکا (Id=1) رو دارند
|
||||||
|
// 3. فیچر چتیکاشون هنوز Notes نداره (یعنی حساب ساخته نشده)
|
||||||
|
var pendingUsers = await _context.UserClubFeatures
|
||||||
|
.Include(ucf => ucf.User)
|
||||||
|
.Include(ucf => ucf.ClubMembership)
|
||||||
|
.Where(ucf =>
|
||||||
|
ucf.ClubFeatureId == (long)ClubFeatureType.Chatika &&
|
||||||
|
ucf.ClubMembership.IsActive &&
|
||||||
|
!ucf.IsDeleted &&
|
||||||
|
!ucf.IsActive) // حساب هنوز ساخته نشده
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!pendingUsers.Any())
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"✅ [{ExecutionId}] No pending users for Chatika activation",
|
||||||
|
executionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"📋 [{ExecutionId}] Found {Count} users pending Chatika activation",
|
||||||
|
executionId,
|
||||||
|
pendingUsers.Count);
|
||||||
|
|
||||||
|
var successCount = 0;
|
||||||
|
var failCount = 0;
|
||||||
|
|
||||||
|
foreach (var userFeature in pendingUsers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// بررسی تکراری نبودن (Double-check)
|
||||||
|
if (userFeature.IsActive)
|
||||||
|
{
|
||||||
|
_logger.LogDebug(
|
||||||
|
"⏭️ Skipping user {UserId} - already processed",
|
||||||
|
userFeature.UserId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = userFeature.User;
|
||||||
|
var fullName = $"{user.FirstName} {user.LastName}".Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(fullName)) fullName = user.Mobile;
|
||||||
|
|
||||||
|
// کال کردن API چتیکا با retry
|
||||||
|
var result = await _retryPipeline.ExecuteAsync(
|
||||||
|
async ct => await _chatikaApiService.CreateAccountAsync(
|
||||||
|
user.Mobile,
|
||||||
|
fullName,
|
||||||
|
ct),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
// آپدیت فیچر با توضیحات و URL
|
||||||
|
var description = ChatikaFeatureDescription;
|
||||||
|
if (!string.IsNullOrEmpty(result.AccessUrl))
|
||||||
|
{
|
||||||
|
description = description.Replace(
|
||||||
|
"https://chatika.ir",
|
||||||
|
result.AccessUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
userFeature.Notes = description;
|
||||||
|
userFeature.IsActive = true;
|
||||||
|
userFeature.LastModified = DateTime.Now;
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"✅ Chatika account activated for user {UserId}",
|
||||||
|
userFeature.UserId);
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"⚠️ Failed to create Chatika account for user {UserId}: {Error}",
|
||||||
|
userFeature.UserId,
|
||||||
|
result.ErrorMessage);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex,
|
||||||
|
"❌ Error processing Chatika activation for user {UserId}",
|
||||||
|
userFeature.UserId);
|
||||||
|
failCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"🏁 [{ExecutionId}] Chatika activation job completed. Success: {Success}, Failed: {Failed}",
|
||||||
|
executionId,
|
||||||
|
successCount,
|
||||||
|
failCount);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex,
|
||||||
|
"❌ [{ExecutionId}] Chatika activation job failed",
|
||||||
|
executionId);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -92,9 +92,19 @@ public static class ConfigureServices
|
|||||||
// Commission Calculation Strategy Factory - برای سوییچ بین ORM و SP
|
// Commission Calculation Strategy Factory - برای سوییچ بین ORM و SP
|
||||||
services.AddScoped<ICommissionCalculationStrategyFactory, CommissionCalculationStrategyFactory>();
|
services.AddScoped<ICommissionCalculationStrategyFactory, CommissionCalculationStrategyFactory>();
|
||||||
|
|
||||||
|
// Chatika API Service - سرویس ایجاد حساب چتیکا
|
||||||
|
services.AddHttpClient<IChatikaApiService, ChatikaApiService>()
|
||||||
|
.SetHandlerLifetime(TimeSpan.FromMinutes(5))
|
||||||
|
.ConfigureHttpClient((sp, client) =>
|
||||||
|
{
|
||||||
|
var config = sp.GetRequiredService<IConfiguration>();
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(30);
|
||||||
|
});
|
||||||
|
|
||||||
// Background Workers - Deprecated: Using Hangfire instead
|
// Background Workers - Deprecated: Using Hangfire instead
|
||||||
// services.AddHostedService<WeeklyNetworkCommissionWorker>();
|
// services.AddHostedService<WeeklyNetworkCommissionWorker>();
|
||||||
services.AddScoped<WeeklyCommissionJob>(); // Hangfire Job (Scoped for DI)
|
services.AddScoped<WeeklyCommissionJob>(); // Hangfire Job (Scoped for DI)
|
||||||
|
services.AddScoped<ChatikaAccountActivationJob>(); // Hangfire Job for Chatika activation
|
||||||
|
|
||||||
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
if (configuration.GetValue<bool>("UseInMemoryDatabase"))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ public class ApplicationDbContext : DbContext, IApplicationDbContext
|
|||||||
// Configuration
|
// Configuration
|
||||||
public DbSet<SystemConfiguration> SystemConfigurations => Set<SystemConfiguration>();
|
public DbSet<SystemConfiguration> SystemConfigurations => Set<SystemConfiguration>();
|
||||||
public DbSet<SystemConfigurationHistory> SystemConfigurationHistories => Set<SystemConfigurationHistory>();
|
public DbSet<SystemConfigurationHistory> SystemConfigurationHistories => Set<SystemConfigurationHistory>();
|
||||||
|
public DbSet<AppVersion> AppVersions => Set<AppVersion>();
|
||||||
|
|
||||||
// Club Management
|
// Club Management
|
||||||
public DbSet<ClubMembership> ClubMemberships => Set<ClubMembership>();
|
public DbSet<ClubMembership> ClubMemberships => Set<ClubMembership>();
|
||||||
|
|||||||
+3647
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class u18 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3699
File diff suppressed because it is too large
Load Diff
+48
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAppVersionsTable : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AppVersions",
|
||||||
|
schema: "CMS",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
AppName = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
CurrentVersion = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
MinRequiredVersion = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
RequiresFullCacheClear = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
UpdateMessage = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
ReleaseNotes = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
Created = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
CreatedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
LastModified = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
LastModifiedBy = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||||
|
IsDeleted = table.Column<bool>(type: "bit", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AppVersions", x => x.Id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AppVersions",
|
||||||
|
schema: "CMS");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
@@ -451,6 +451,58 @@ namespace CMSMicroservice.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("WorkerExecutionLogs", "CMS");
|
b.ToTable("WorkerExecutionLogs", "CMS");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.AppVersion", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("AppName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Created")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("CreatedBy")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("CurrentVersion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("IsDeleted")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastModified")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<string>("LastModifiedBy")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("MinRequiredVersion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ReleaseNotes")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("RequiresFullCacheClear")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("UpdateMessage")
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("AppVersions", "CMS");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.SystemConfiguration", b =>
|
modelBuilder.Entity("CMSMicroservice.Domain.Entities.Configuration.SystemConfiguration", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CMSMicroservice.Application.Common.Interfaces;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// پیادهسازی سرویس چتیکا با HttpClient
|
||||||
|
/// API: POST /api/v1/organizations/register-user
|
||||||
|
/// </summary>
|
||||||
|
public class ChatikaApiService : IChatikaApiService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly ILogger<ChatikaApiService> _logger;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
private readonly string _apiKey;
|
||||||
|
|
||||||
|
public ChatikaApiService(
|
||||||
|
HttpClient httpClient,
|
||||||
|
ILogger<ChatikaApiService> logger,
|
||||||
|
IConfiguration configuration)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// تنظیمات از appsettings.json
|
||||||
|
_baseUrl = configuration["Chatika:BaseUrl"] ?? "https://api.chatika.ir";
|
||||||
|
_apiKey = configuration["Chatika:ApiKey"] ?? "";
|
||||||
|
|
||||||
|
// تنظیم HttpClient
|
||||||
|
_httpClient.BaseAddress = new Uri(_baseUrl);
|
||||||
|
if (!string.IsNullOrEmpty(_apiKey))
|
||||||
|
{
|
||||||
|
_httpClient.DefaultRequestHeaders.Add("X-API-Key", _apiKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ChatikaAccountResult> CreateAccountAsync(
|
||||||
|
string mobileNumber,
|
||||||
|
string fullName,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation(
|
||||||
|
"🤖 Creating Chatika account for mobile: {Mobile}",
|
||||||
|
mobileNumber.Substring(0, 4) + "***");
|
||||||
|
|
||||||
|
var request = new ChatikaRegisterRequest
|
||||||
|
{
|
||||||
|
MobileNumber = mobileNumber
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(
|
||||||
|
"/api/v1/organizations/register-user",
|
||||||
|
request,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var result = await response.Content.ReadFromJsonAsync<ChatikaRegisterResponse>(
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"✅ Chatika account created successfully. UserId: {UserId}, IsNewUser: {IsNew}, Credit: {Credit}",
|
||||||
|
result?.Id,
|
||||||
|
result?.IsNewUser,
|
||||||
|
result?.CreditCharged);
|
||||||
|
|
||||||
|
return ChatikaAccountResult.Success(
|
||||||
|
chatikaUserId: result?.Id.ToString(),
|
||||||
|
accessUrl: "https://chatika.ir"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
var errorResponse = JsonSerializer.Deserialize<ChatikaErrorResponse>(errorContent);
|
||||||
|
|
||||||
|
_logger.LogWarning(
|
||||||
|
"⚠️ Chatika API returned error. Status: {Status}, Code: {ErrorCode}, Message: {Message}",
|
||||||
|
response.StatusCode,
|
||||||
|
errorResponse?.ErrorCode,
|
||||||
|
errorResponse?.Message);
|
||||||
|
|
||||||
|
return ChatikaAccountResult.Failure($"{errorResponse?.ErrorCode}: {errorResponse?.Message}");
|
||||||
|
}
|
||||||
|
catch (HttpRequestException ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "❌ Network error calling Chatika API");
|
||||||
|
return ChatikaAccountResult.Failure($"خطای شبکه: {ex.Message}");
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "❌ Timeout calling Chatika API");
|
||||||
|
return ChatikaAccountResult.Failure("تایماوت در ارتباط با سرویس چتیکا");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "❌ Unexpected error calling Chatika API");
|
||||||
|
return ChatikaAccountResult.Failure($"خطای غیرمنتظره: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request model برای ثبت کاربر در چتیکا
|
||||||
|
/// </summary>
|
||||||
|
private class ChatikaRegisterRequest
|
||||||
|
{
|
||||||
|
[JsonPropertyName("mobile_number")]
|
||||||
|
public string MobileNumber { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model موفق از چتیکا
|
||||||
|
/// </summary>
|
||||||
|
private class ChatikaRegisterResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("mobile_number")]
|
||||||
|
public string MobileNumber { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("organization_id")]
|
||||||
|
public long OrganizationId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("organization_title")]
|
||||||
|
public string OrganizationTitle { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("wallet_balance")]
|
||||||
|
public decimal WalletBalance { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("is_new_user")]
|
||||||
|
public bool IsNewUser { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("credit_charged")]
|
||||||
|
public decimal CreditCharged { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Response model خطا از چتیکا
|
||||||
|
/// </summary>
|
||||||
|
private class ChatikaErrorResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("error_code")]
|
||||||
|
public string ErrorCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<Version>0.0.156</Version>
|
<Version>0.0.159</Version>
|
||||||
<DebugType>None</DebugType>
|
<DebugType>None</DebugType>
|
||||||
<DebugSymbols>False</DebugSymbols>
|
<DebugSymbols>False</DebugSymbols>
|
||||||
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
|
||||||
@@ -57,6 +57,8 @@
|
|||||||
<Protobuf Include="Protos\discountorder.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
<Protobuf Include="Protos\discountorder.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||||
<!-- Geography System (GMS) -->
|
<!-- Geography System (GMS) -->
|
||||||
<Protobuf Include="Protos\city.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
<Protobuf Include="Protos\city.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||||
|
<!-- App Version Tracking System -->
|
||||||
|
<Protobuf Include="Protos\appversion.proto" ProtoRoot="Protos\" GrpcServices="Both" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="PushToFoursatNuget" AfterTargets="Pack">
|
<Target Name="PushToFoursatNuget" AfterTargets="Pack">
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package appversion;
|
||||||
|
|
||||||
|
import "google/protobuf/empty.proto";
|
||||||
|
import "google/protobuf/wrappers.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
import "google/api/annotations.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "CMSMicroservice.Protobuf.Protos.AppVersion";
|
||||||
|
|
||||||
|
// Service for tracking app versions and cache invalidation
|
||||||
|
service AppVersionContract
|
||||||
|
{
|
||||||
|
// Get current version for an app - called by frontends to check if cache clear is needed
|
||||||
|
rpc GetAppVersion(GetAppVersionRequest) returns (GetAppVersionResponse){
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/AppVersion/Get"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update/Create app version - called by admin when deploying new version
|
||||||
|
rpc UpdateAppVersion(UpdateAppVersionRequest) returns (google.protobuf.Empty){
|
||||||
|
option (google.api.http) = {
|
||||||
|
post: "/AppVersion/Update"
|
||||||
|
body: "*"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all app versions
|
||||||
|
rpc GetAllAppVersions(GetAllAppVersionsRequest) returns (GetAllAppVersionsResponse){
|
||||||
|
option (google.api.http) = {
|
||||||
|
get: "/AppVersion/GetAll"
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request to get current version for specific app
|
||||||
|
message GetAppVersionRequest
|
||||||
|
{
|
||||||
|
string app_name = 1; // e.g., "FrontOffice", "BackOffice", "Mobile"
|
||||||
|
google.protobuf.StringValue current_client_version = 2; // Current version on client for comparison
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response with version info
|
||||||
|
message GetAppVersionResponse
|
||||||
|
{
|
||||||
|
bool found = 1; // Whether the app was found
|
||||||
|
string app_name = 2;
|
||||||
|
string current_version = 3;
|
||||||
|
string min_required_version = 4;
|
||||||
|
bool requires_full_cache_clear = 5;
|
||||||
|
bool requires_update = 6; // True if client version < min_required_version
|
||||||
|
google.protobuf.StringValue update_message = 7;
|
||||||
|
google.protobuf.StringValue release_notes = 8;
|
||||||
|
google.protobuf.Timestamp last_updated = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request to update/create app version
|
||||||
|
message UpdateAppVersionRequest
|
||||||
|
{
|
||||||
|
string app_name = 1;
|
||||||
|
string current_version = 2;
|
||||||
|
google.protobuf.StringValue min_required_version = 3;
|
||||||
|
bool requires_full_cache_clear = 4;
|
||||||
|
google.protobuf.StringValue update_message = 5;
|
||||||
|
google.protobuf.StringValue release_notes = 6;
|
||||||
|
google.protobuf.StringValue update_reason = 7; // For admin logs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request to get all app versions
|
||||||
|
message GetAllAppVersionsRequest
|
||||||
|
{
|
||||||
|
bool include_inactive = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response with all app versions
|
||||||
|
message GetAllAppVersionsResponse
|
||||||
|
{
|
||||||
|
repeated AppVersionItem items = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AppVersionItem
|
||||||
|
{
|
||||||
|
int64 id = 1;
|
||||||
|
string app_name = 2;
|
||||||
|
string current_version = 3;
|
||||||
|
string min_required_version = 4;
|
||||||
|
bool requires_full_cache_clear = 5;
|
||||||
|
string update_message = 6;
|
||||||
|
string release_notes = 7;
|
||||||
|
bool is_active = 8;
|
||||||
|
google.protobuf.Timestamp created = 9;
|
||||||
|
google.protobuf.Timestamp last_modified = 10;
|
||||||
|
}
|
||||||
@@ -178,6 +178,7 @@ message NetworkTreeNodeModel
|
|||||||
google.protobuf.Int64Value activation_week_definition_id = 10; // شماره هفته فعالسازی
|
google.protobuf.Int64Value activation_week_definition_id = 10; // شماره هفته فعالسازی
|
||||||
bool is_activated_in_target_week = 11; // آیا در هفته هدف فعال شده
|
bool is_activated_in_target_week = 11; // آیا در هفته هدف فعال شده
|
||||||
google.protobuf.Timestamp user_created = 12; // تاریخ ایجاد کاربر
|
google.protobuf.Timestamp user_created = 12; // تاریخ ایجاد کاربر
|
||||||
|
string referral_code = 13; // کد معرف کاربر
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetHistory Query
|
// GetHistory Query
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ public class NetworkMembershipProfile : IRegister
|
|||||||
{
|
{
|
||||||
UserId = node.UserId,
|
UserId = node.UserId,
|
||||||
UserName = $"{node.FirstName} {node.LastName}".Trim(),
|
UserName = $"{node.FirstName} {node.LastName}".Trim(),
|
||||||
|
ReferralCode = node.ReferralCode ?? string.Empty,
|
||||||
NetworkLeg = (int)(node.LegPosition ?? NetworkLeg.Left),
|
NetworkLeg = (int)(node.LegPosition ?? NetworkLeg.Left),
|
||||||
NetworkLevel = node.CurrentDepth,
|
NetworkLevel = node.CurrentDepth,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
|
|||||||
@@ -223,6 +223,14 @@ using (var scope = app.Services.CreateScope())
|
|||||||
// Daya Loan Check: Every 15 minutes
|
// Daya Loan Check: Every 15 minutes
|
||||||
CMSMicroservice.WebApi.Workers.DayaLoanCheckWorker.Schedule(recurringJobManager);
|
CMSMicroservice.WebApi.Workers.DayaLoanCheckWorker.Schedule(recurringJobManager);
|
||||||
app.Logger.LogInformation("✅ Hangfire recurring job 'daya-loan-check' registered (Cron: */15 * * * * - Every 15 minutes)");
|
app.Logger.LogInformation("✅ Hangfire recurring job 'daya-loan-check' registered (Cron: */15 * * * * - Every 15 minutes)");
|
||||||
|
|
||||||
|
// Chatika Account Activation: Every 5 minutes
|
||||||
|
recurringJobManager.AddOrUpdate<CMSMicroservice.Infrastructure.BackgroundJobs.ChatikaAccountActivationJob>(
|
||||||
|
recurringJobId: "chatika-account-activation",
|
||||||
|
methodCall: job => job.ExecuteAsync(CancellationToken.None),
|
||||||
|
cronExpression: "*/5 * * * *",
|
||||||
|
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
|
||||||
|
app.Logger.LogInformation("✅ Hangfire recurring job 'chatika-account-activation' registered (Cron: */5 * * * * - Every 5 minutes)");
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using CMSMicroservice.Protobuf.Protos.AppVersion;
|
||||||
|
using CMSMicroservice.WebApi.Common.Services;
|
||||||
|
using CMSMicroservice.Application.AppVersionCQ.Queries.GetAppVersion;
|
||||||
|
using CMSMicroservice.Application.AppVersionCQ.Queries.GetAllAppVersions;
|
||||||
|
using CMSMicroservice.Application.AppVersionCQ.Commands.UpdateAppVersion;
|
||||||
|
|
||||||
|
namespace CMSMicroservice.WebApi.Services;
|
||||||
|
|
||||||
|
public class AppVersionService : AppVersionContract.AppVersionContractBase
|
||||||
|
{
|
||||||
|
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
||||||
|
|
||||||
|
public AppVersionService(IDispatchRequestToCQRS dispatchRequestToCQRS)
|
||||||
|
{
|
||||||
|
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetAppVersionResponse> GetAppVersion(GetAppVersionRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
return await _dispatchRequestToCQRS.Handle<GetAppVersionRequest, GetAppVersionQuery, GetAppVersionResponse>(request, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<Empty> UpdateAppVersion(UpdateAppVersionRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
return await _dispatchRequestToCQRS.Handle<UpdateAppVersionRequest, UpdateAppVersionCommand>(request, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<GetAllAppVersionsResponse> GetAllAppVersions(GetAllAppVersionsRequest request, ServerCallContext context)
|
||||||
|
{
|
||||||
|
return await _dispatchRequestToCQRS.Handle<GetAllAppVersionsRequest, GetAllAppVersionsQuery, GetAllAppVersionsResponse>(request, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,12 +39,17 @@ public class DayaLoanCheckWorker
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// پیدا کردن کاربرانی که اعتبار دایا را دریافت نکردهاند
|
|
||||||
|
// پیدا کردن کاربرانی که:
|
||||||
|
// 1. اعتبار دایا را دریافت نکردهاند
|
||||||
|
// 2. کد ملی دارند
|
||||||
|
// 3. قبلاً شماره قرارداد نگرفتهاند
|
||||||
var pendingUsers = await _context.Users
|
var pendingUsers = await _context.Users
|
||||||
.Where(u =>
|
.Where(u =>
|
||||||
u.HasReceivedDayaCredit == false &&
|
u.HasReceivedDayaCredit == false &&
|
||||||
u.NationalCode != null &&
|
u.NationalCode != null &&
|
||||||
u.NationalCode != "")
|
u.NationalCode != ""
|
||||||
|
)
|
||||||
.Select(u => new { u.Id, u.NationalCode })
|
.Select(u => new { u.Id, u.NationalCode })
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning",
|
|
||||||
"Microsoft.EntityFrameworkCore": "Warning"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Server=YOUR_PRODUCTION_SERVER;Database=FourSat_CMS;User Id=YOUR_USER;Password=YOUR_PASSWORD;TrustServerCertificate=True;MultipleActiveResultSets=true"
|
|
||||||
},
|
|
||||||
"Email": {
|
|
||||||
"Enabled": true,
|
|
||||||
"SmtpHost": "smtp.gmail.com",
|
|
||||||
"SmtpPort": 587,
|
|
||||||
"SmtpUsername": "your-production-email@gmail.com",
|
|
||||||
"SmtpPassword": "your-gmail-app-password",
|
|
||||||
"FromEmail": "noreply@foursat.com",
|
|
||||||
"FromName": "FourSat CMS",
|
|
||||||
"EnableSsl": true
|
|
||||||
},
|
|
||||||
"Sms": {
|
|
||||||
"Enabled": true,
|
|
||||||
"Provider": "Kavenegar",
|
|
||||||
"KavenegarApiKey": "YOUR_PRODUCTION_KAVENEGAR_API_KEY",
|
|
||||||
"Sender": "10008663"
|
|
||||||
},
|
|
||||||
"Jwt": {
|
|
||||||
"Issuer": "https://api.foursat.com",
|
|
||||||
"Audience": "https://foursat.com",
|
|
||||||
"SecretKey": "YOUR_PRODUCTION_SECRET_KEY_MINIMUM_32_CHARACTERS_LONG"
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*"
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"JwtAudience": "https://localhost",
|
"JwtAudience": "https://localhost",
|
||||||
"JwtExpiryInDays": 5,
|
"JwtExpiryInDays": 5,
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
"DefaultConnection": "Data Source=194.5.195.53,31433; Initial Catalog=Foursat;User ID=sa;Password=YourStrong@Passw0rd;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
|
"DefaultConnection": "Data Source=194.5.195.53,31433; Initial Catalog=Foursat;User ID=sa;Password=87zH26nbqT;Connection Timeout=300000;MultipleActiveResultSets=True;Encrypt=False",
|
||||||
"providerName": "System.Data.SqlClient"
|
"providerName": "System.Data.SqlClient"
|
||||||
},
|
},
|
||||||
"Otp": {
|
"Otp": {
|
||||||
@@ -50,6 +50,11 @@
|
|||||||
"MerchantPermissionKey": "56146364$04sXjethI5WxhItR1Q9xnmFdJzl2BB8Bclsq8dAy7YVSZp3vtt-wP7ivrcCvmKLq",
|
"MerchantPermissionKey": "56146364$04sXjethI5WxhItR1Q9xnmFdJzl2BB8Bclsq8dAy7YVSZp3vtt-wP7ivrcCvmKLq",
|
||||||
"CacheDurationMinutes": 20
|
"CacheDurationMinutes": 20
|
||||||
},
|
},
|
||||||
|
"Chatika": {
|
||||||
|
"Enabled": true,
|
||||||
|
"BaseUrl": "https://api.chatika.ir",
|
||||||
|
"ApiKey": "tIukvL8dnV4cB3yVWcCD9Xyfbj8rBxm5wPt2mLyJCgTsBBoMTWjt6mFEqQwpw-er"
|
||||||
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"EndpointDefaults": {
|
"EndpointDefaults": {
|
||||||
@@ -61,7 +66,7 @@
|
|||||||
"Audience": "domain_api"
|
"Audience": "domain_api"
|
||||||
},
|
},
|
||||||
"Seq": {
|
"Seq": {
|
||||||
"ServerUrl": "http://seq-svc:5341",
|
"ServerUrl": "https://seq.afrino.co",
|
||||||
"ApiKey": ""
|
"ApiKey": "oxpvpUzU1pZxMS4s3Fqq"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user