diff --git a/.github/workflows/auto-cd.yml b/.github/workflows/auto-cd.yml new file mode 100644 index 0000000..d88435e --- /dev/null +++ b/.github/workflows/auto-cd.yml @@ -0,0 +1,289 @@ +name: CD - Build and Deploy on VPS + +on: + push: + branches: + - prod + - main + workflow_dispatch: # Allow manual trigger + +env: + REGISTRY: docker.io + IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/taskgenix-be + +jobs: + build-and-push: + runs-on: self-hosted + permissions: + contents: read + packages: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create Firebase service account file + env: + FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} + run: | + echo "$FIREBASE_SERVICE_ACCOUNT" > firebase-service-account.json + + # Verify the file is valid JSON + if ! jq . firebase-service-account.json >/dev/null 2>&1; then + echo "❌ Error: firebase-service-account.json is not valid JSON" + cat firebase-service-account.json + exit 1 + fi + + echo "✅ Firebase service account file created successfully" + ls -lh firebase-service-account.json + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Generate image tags + id: tags + run: | + BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + SHORT_SHA=$(git rev-parse --short HEAD) + + # Sanitize branch name for Docker tag (replace / with -) + SAFE_BRANCH=$(echo "$BRANCH_NAME" | sed 's/\//-/g') + + echo "branch=${SAFE_BRANCH}" >> $GITHUB_OUTPUT + echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + + # Build tags + TAGS="${{ env.IMAGE_NAME }}:latest" + TAGS="${TAGS},${{ env.IMAGE_NAME }}:${SAFE_BRANCH}" + TAGS="${TAGS},${{ env.IMAGE_NAME }}:${SAFE_BRANCH}-${SHORT_SHA}" + + echo "tags=${TAGS}" >> $GITHUB_OUTPUT + + echo "Generated tags:" + echo "$TAGS" | tr ',' '\n' + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile.prod + # Push only on push event, PR only build + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max + secret-files: | + firebase_service_account=./firebase-service-account.json + + - name: Comment on PR with build info + if: github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: build-status + message: | + ## 🐳 Docker Build Complete + + **Image would be tagged as:** + - `${{ env.IMAGE_NAME }}:latest` + - `${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.branch }}` + - `${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.branch }}-${{ steps.tags.outputs.sha }}` + + **Tag explanations:** + - latest - Always points to the most recent build + - {branch-name} - Tracks the specific branch + - {branch-name}-{short-sha} - Unique identifier for this exact commit + + **Note:** Image is built but not pushed for pull requests. + + 📦 [View on Docker Hub](https://hub.docker.com/repository/docker/lcaohoanq/taskgenix-be) + + - name: Image digest + if: github.event_name != 'pull_request' + run: echo ${{ steps.build.outputs.digest }} + + - name: Clean up sensitive files + if: always() + run: | + rm -f firebase-service-account.json + echo "🧹 Cleaned up sensitive files" + + deploy: + needs: build-and-push + runs-on: self-hosted + if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/prod' || github.ref == 'refs/heads/main') + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set environment + id: env + run: | + if [[ "${{ github.ref }}" == "refs/heads/prod" ]]; then + echo "environment=production" >> $GITHUB_OUTPUT + echo "compose_file=docker-compose.prod.yml" >> $GITHUB_OUTPUT + else + echo "environment=staging" >> $GITHUB_OUTPUT + echo "compose_file=docker-compose.dev.yml" >> $GITHUB_OUTPUT + fi + + - name: Create .env file + run: | + cat > .env << EOF + Jwt__SecretKey=${{ secrets.JWT__SECRETKEY }} + Jwt__Issuer=${{ secrets.JWT__ISSUER }} + Jwt__Audience=${{ secrets.JWT__AUDIENCE }} + ConnectionStrings__SupabaseConnection=${{ secrets.CONNECTIONSTRINGS__SUPABASECONNECTION }} + Jwt__RefreshSecretKey=${{ secrets.JWT__REFRESHSECRETKEY }} + Auth0__InternalSyncSecret=${{ secrets.AUTH0__INTERNALSYNCSECRET }} + PayOS__ClientId=${{ secrets.PAYOS__CLIENTID }} + PayOS__ApiKey=${{ secrets.PAYOS__APIKEY }} + PayOS__ChecksumKey=${{ secrets.PAYOS__CHECKSUMKEY }} + B2Storage__ServiceUrl=${{ secrets.B2STORAGE__SERVICEURL }} + B2Storage__BucketName=${{ secrets.B2STORAGE__BUCKETNAME }} + B2Storage__AccessKeyId=${{ secrets.B2STORAGE__ACCESSKEYID }} + B2Storage__SecretAccessKey=${{ secrets.B2STORAGE__SECRETACCESSKEY }} + AI__Gemini__Summarize__APIKey=${{ secrets.AI__GEMINI__SUMMARIZE__APIKEY }} + AI__Gemini__TaskGen__APIKey=${{ secrets.AI__GEMINI__TASKGEN__APIKEY }} + CloudflareR2__AccessKeyId=${{ secrets.CLOUDFLARER2__ACCESSKEYID }} + CloudflareR2__SecretAccessKey=${{ secrets.CLOUDFLARER2__SECRETACCESSKEY }} + CloudflareR2__ServiceUrl=${{ secrets.CLOUDFLARER2__SERVICEURL }} + CloudflareR2__BucketName=${{ secrets.CLOUDFLARER2__BUCKETNAME }} + EOF + + - name: Create docker-compose file + run: | + cat > docker-compose.prod.yml << 'EOF' + networks: + taskgenix_network: + driver: bridge + + services: + taskgenix-server-prod: + container_name: taskgenix-server + image: lcaohoanq/taskgenix-be:latest + ports: + - "8386:5009" + env_file: + - .env + environment: + - Jwt__ExpirationInMinutes=60 + - Jwt__RefreshExpirationInDays=182 + - AllowedHosts=* + - ApplyMigrations=false + - "BaseURL=com.app.tgx:" + - AI__Gemini__Summarize__Model=gemini-2.0-flash + - AI__Gemini__TaskGen__Model=gemini-2.0-flash + volumes: + - /home/lcaohoanq/dulieu/taskgenix/firebase-service-account.json:/app/firebase-service-account.json:ro + restart: always + networks: + - taskgenix_network + EOF + + - name: Pull latest image + run: | + echo "🔄 Pulling latest Docker image..." + docker pull ${{ env.IMAGE_NAME }}:latest + + - name: Stop existing containers + run: | + echo "🛑 Stopping existing containers..." + docker compose -f docker-compose.prod.yml down || true + + - name: Deploy application + run: | + echo "🚀 Deploying to ${{ steps.env.outputs.environment }} environment..." + docker compose -f docker-compose.prod.yml up -d --build --remove-orphans + + # - name: Wait for container to be healthy + # run: | + # echo "⏳ Waiting for container to be healthy..." + # timeout=60 + # elapsed=0 + + # while [ $elapsed -lt $timeout ]; do + # if docker ps | grep -q taskgenix-server; then + # if [ "$(docker inspect --format='{{.State.Health.Status}}' taskgenix-server 2>/dev/null)" = "healthy" ]; then + # echo "✅ Container is healthy!" + # break + # fi + # fi + + # echo "Waiting... ($elapsed/$timeout seconds)" + # sleep 5 + # elapsed=$((elapsed + 5)) + # done + + # if [ $elapsed -ge $timeout ]; then + # echo "❌ Container failed to become healthy within $timeout seconds" + # docker logs taskgenix-server --tail 50 + # exit 1 + # fi + + - name: Verify deployment + run: | + echo "🔍 Verifying deployment..." + docker ps -a | grep taskgenix-server + + echo "📋 Container logs (last 20 lines):" + docker logs taskgenix-server --tail 20 + + - name: Clean up old images + run: | + echo "🧹 Cleaning up old Docker images..." + docker image prune -af --filter "until=24h" || true + + notify: + needs: [build-and-push, deploy] + runs-on: ubuntu-latest + if: always() + permissions: + pull-requests: write + + steps: + - name: Comment on PR - Success + if: github.event_name == 'pull_request' && needs.build-and-push.result == 'success' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: build-result + message: | + ## ✅ Build Successful! + + Your changes have been built successfully. + + 📦 [View on Docker Hub](https://hub.docker.com/repository/docker/lcaohoanq/taskgenix-be) + + - name: Comment on PR - Failure + if: github.event_name == 'pull_request' && needs.build-and-push.result == 'failure' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: build-result + message: | + ## ❌ Build Failed! + + Please check the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + + - name: Notify deployment success + if: github.event_name == 'push' && needs.deploy.result == 'success' + run: | + echo "✅ Deployment successful!" + echo "Environment: ${{ needs.deploy.outputs.environment }}" + echo "Image: ${{ env.IMAGE_NAME }}:latest" + # Add notification logic here (Slack, Discord, Email, etc.) + + - name: Notify deployment failure + if: github.event_name == 'push' && needs.deploy.result == 'failure' + run: | + echo "❌ Deployment failed!" + echo "Check the logs for details" + # Add failure notification logic here diff --git a/.github/workflows/cd-v2.yml b/.github/workflows/cd-v2.yml new file mode 100644 index 0000000..9f3c29e --- /dev/null +++ b/.github/workflows/cd-v2.yml @@ -0,0 +1,214 @@ +name: CD - Build and Deploy + +on: + # push: + # branches: + # - main + # - dev + # pull_request: + # branches: + # - main + workflow_dispatch: # Allow manual trigger + +env: + REGISTRY: docker.io + IMAGE_NAME: taskgenix/taskgenix-be + DOTNET_VERSION: "8.0.x" + +jobs: + # Code formatting check for pull requests + format-check: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Restore dependencies + run: dotnet restore + + - name: Check code formatting + id: format + run: | + dotnet format --verify-no-changes --verbosity diagnostic + if [ $? -ne 0 ]; then + echo "FAILED=true" >> $GITHUB_ENV + exit 1 + fi + continue-on-error: true + + - name: Comment on PR if format fails + if: env.FAILED == 'true' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: format-check + message: | + ❌ Code formatting check failed. + + Run this command to fix: + ```bash + dotnet format + ``` + + - name: Fail workflow if format check failed + if: env.FAILED == 'true' + run: exit 1 + + build-and-push: + runs-on: ubuntu-latest + needs: [format-check] + if: always() && (needs.format-check.result == 'success' || needs.format-check.result == 'skipped') + permissions: + contents: read + packages: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Full history for versioning + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + # Generate version and metadata + - name: Generate version info + id: version + run: | + GIT_SHORT_SHA=$(git rev-parse --short HEAD) + DATE=$(date +'%Y%m%d') + BRANCH_NAME=${GITHUB_REF#refs/heads/} + + echo "GIT_SHORT_SHA=${GIT_SHORT_SHA}" >> $GITHUB_ENV + echo "DATE=${DATE}" >> $GITHUB_ENV + echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_ENV + + echo "git_sha=${GIT_SHORT_SHA}" >> $GITHUB_OUTPUT + echo "date=${DATE}" >> $GITHUB_OUTPUT + echo "branch=${BRANCH_NAME}" >> $GITHUB_OUTPUT + + # Extract metadata for non-main branches + - name: Extract metadata (dev/PR) + if: github.ref != 'refs/heads/main' + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha,prefix={{branch}}- + + # Build and push for main branch with custom tags + - name: Build and push Docker image (main branch) + if: github.ref == 'refs/heads/main' + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile.prod + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ env.IMAGE_NAME }}:latest + ${{ env.IMAGE_NAME }}:${{ env.GIT_SHORT_SHA }} + ${{ env.IMAGE_NAME }}:${{ env.DATE }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # Build and push for non-main branches with metadata + - name: Build and push Docker image (dev/PR) + if: github.ref != 'refs/heads/main' + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile.prod + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Comment on PR with image info + if: github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: docker-build + message: | + 🐳 Docker image build completed! + + **Image tags:** + ``` + ${{ steps.meta.outputs.tags }} + ``` + + **Commit:** `${{ env.GIT_SHORT_SHA }}` + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') + + steps: + - name: Deploy to staging (dev branch) + if: github.ref == 'refs/heads/dev' + run: | + echo "🚀 Deploying to staging environment..." + # Add your staging deployment commands here + # Example: SSH to staging server and update containers + # ssh user@staging-server 'docker pull ${{ env.IMAGE_NAME }}:dev && docker-compose up -d' + + - name: Deploy to production (main branch) + if: github.ref == 'refs/heads/main' + run: | + echo "🚀 Deploying to production environment..." + # Add your production deployment commands here + # Example: SSH to production server and update containers + # ssh user@prod-server 'docker pull ${{ env.IMAGE_NAME }}:latest && docker-compose up -d' + + - name: Comment on PR after deployment + if: github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: deployment + message: | + ✅ Deployment completed successfully! + + **Environment:** ${{ github.ref == 'refs/heads/main' && 'Production' || 'Staging' }} + **Image:** `${{ env.IMAGE_NAME }}:${{ env.GIT_SHORT_SHA }}` + + notify: + needs: [build-and-push, deploy] + runs-on: ubuntu-latest + if: always() && github.event_name != 'pull_request' + + steps: + - name: Notify success + if: needs.build-and-push.result == 'success' && (needs.deploy.result == 'success' || needs.deploy.result == 'skipped') + run: | + echo "✅ Build and deployment successful!" + # Add notification logic here (Slack, Discord, Email, etc.) + # curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} -H 'Content-Type: application/json' \ + # -d '{"text":"✅ Deployment successful for ${{ github.ref }}"}' + + - name: Notify failure + if: needs.build-and-push.result == 'failure' || needs.deploy.result == 'failure' + run: | + echo "❌ Build or deployment failed!" + # Add failure notification logic here + # curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} -H 'Content-Type: application/json' \ + # -d '{"text":"❌ Deployment failed for ${{ github.ref }}"}' diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..e96d053 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,192 @@ +name: CD - Build and Deploy + +on: + push: + branches: + - main #production + - dev #staging + - prod #real deployment + - feature/no-issue/add-cd-workflow + pull_request: + branches: + - main + - dev + - prod + workflow_dispatch: # Allow manual trigger + +env: + REGISTRY: docker.io + IMAGE_NAME: ${{ secrets.DOCKER_USERNAME }}/taskgenix-be + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + pull-requests: write # bot to comment on PR + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create Firebase service account file + env: + FIREBASE_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_SERVICE_ACCOUNT }} + run: | + echo "$FIREBASE_SERVICE_ACCOUNT" > firebase-service-account.json + + # Verify the file is valid JSON + if ! jq . firebase-service-account.json >/dev/null 2>&1; then + echo "❌ Error: firebase-service-account.json is not valid JSON" + cat firebase-service-account.json + exit 1 + fi + + echo "✅ Firebase service account file created successfully" + ls -lh firebase-service-account.json + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Generate image tags + id: tags + run: | + BRANCH_NAME=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}} + SHORT_SHA=$(git rev-parse --short HEAD) + + # Sanitize branch name for Docker tag (replace / with -) + SAFE_BRANCH=$(echo "$BRANCH_NAME" | sed 's/\//-/g') + + echo "branch=${SAFE_BRANCH}" >> $GITHUB_OUTPUT + echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT + + # Build tags + TAGS="${{ env.IMAGE_NAME }}:latest" + TAGS="${TAGS},${{ env.IMAGE_NAME }}:${SAFE_BRANCH}" + TAGS="${TAGS},${{ env.IMAGE_NAME }}:${SAFE_BRANCH}-${SHORT_SHA}" + + echo "tags=${TAGS}" >> $GITHUB_OUTPUT + + echo "Generated tags:" + echo "$TAGS" | tr ',' '\n' + + - name: Build and push Docker image + id: build + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile.prod + platforms: linux/amd64,linux/arm64 + # Push only on push branch event, PR only build + push: ${{ github.event_name != 'pull_request' }} + # Take the generated tags from previous step + tags: ${{ steps.tags.outputs.tags }} + cache-from: type=gha + cache-to: type=gha,mode=max + secret-files: | + firebase_service_account=./firebase-service-account.json + + - name: Comment on PR with build info + if: github.event_name == 'pull_request' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: build-status + message: | + ## 🐳 Docker Build Complete + + **Image would be tagged as:** + - `${{ env.IMAGE_NAME }}:latest` + - `${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.branch }}` + - `${{ env.IMAGE_NAME }}:${{ steps.tags.outputs.branch }}-${{ steps.tags.outputs.sha }}` + + ** Tag explanations:** + - latest - Always points to the most recent build (easy to pull) + - {branch-name} - Tracks the specific branch (e.g., main, dev, feature-no-issue-add-cd-workflow) + - {branch-name}-{short-sha} - Unique identifier for this exact commit + + ** Example: ** + - lcaohoanq/taskgenix-be:latest + - lcaohoanq/taskgenix-be:feature-no-issue-add-cd-workflow + - lcaohoanq/taskgenix-be:feature-no-issue-add-cd-workflow-a1b2c3d + + **Note:** Image is built but not pushed for pull requests. + + 📦 [View on Docker Hub](https://hub.docker.com/repository/docker/lcaohoanq/taskgenix-be) + + - name: Image digest + if: github.event_name != 'pull_request' + run: echo ${{ steps.build.outputs.digest }} + + - name: Clean up sensitive files + if: always() + run: | + rm -f firebase-service-account.json + echo "🧹 Cleaned up sensitive files" + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/prod' || github.ref == 'refs/heads/staging') + + steps: + - name: Deploy to staging (staging branch) + if: github.ref == 'refs/heads/staging' + run: | + echo "🚀 Deploying to staging environment..." + # Example: docker pull ${{ env.IMAGE_NAME }}:staging && docker-compose -f docker-compose.staging.yml up -d + + - name: Deploy to production (prod branch) + if: github.ref == 'refs/heads/prod' + run: | + echo "🚀 Deploying to production environment..." + # Example: docker pull ${{ env.IMAGE_NAME }}:latest && docker-compose -f docker-compose.prod.yml up -d + + notify: + needs: [build-and-push, deploy] + runs-on: ubuntu-latest + if: always() + permissions: + pull-requests: write # bot to comment on PR + + steps: + - name: Comment on PR - Success + if: github.event_name == 'pull_request' && needs.build-and-push.result == 'success' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: build-result + message: | + ## ✅ Build Successful! + + Your changes have been built successfully. + + 📦 [View on Docker Hub](https://hub.docker.com/repository/docker/lcaohoanq/taskgenix-be) + + - name: Comment on PR - Failure + if: github.event_name == 'pull_request' && needs.build-and-push.result == 'failure' + uses: marocchino/sticky-pull-request-comment@v2.9.4 + with: + header: build-result + message: | + ## ❌ Build Failed! + + Please check the [workflow logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details. + + - name: Notify push success + if: github.event_name == 'push' && needs.build-and-push.result == 'success' + run: | + echo "✅ Build and push successful!" + echo "Image: ${{ env.IMAGE_NAME }}:latest" + # Add notification logic here (Slack, Discord, Email, etc.) + + - name: Notify push failure + if: github.event_name == 'push' && needs.build-and-push.result == 'failure' + run: | + echo "❌ Build and push failed!" + # Add failure notification logic here diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml new file mode 100644 index 0000000..907b71c --- /dev/null +++ b/.github/workflows/ci-test.yml @@ -0,0 +1,107 @@ +name: CI Test + +on: + workflow_dispatch: + pull_request: + branches: [main, dev] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Restore dependencies + run: dotnet restore + + - name: Build solution + run: dotnet build --configuration Release + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build + path: . + retention-days: 1 + + unit-tests: + runs-on: ubuntu-latest + needs: build + permissions: + checks: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: build + path: . + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Run Unit Tests + run: dotnet test TaskGenix.Tests.Unit/TaskGenix.Tests.Unit.csproj --no-build --configuration Release --logger "trx;LogFileName=unit-test-results.trx" --results-directory TestResults + + - name: Upload Unit Test Results + uses: actions/upload-artifact@v4 + with: + name: unit-test-results + path: TestResults/*.trx + + - name: Unit Test Report + uses: dorny/test-reporter@v1 + if: always() + with: + name: Unit Test Results + path: TestResults/*.trx + reporter: dotnet-trx + + integration-tests: + runs-on: ubuntu-latest + needs: unit-tests + if: github.base_ref == 'main' + permissions: + checks: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: build + path: . + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Run Integration Tests + run: dotnet test TaskGenix.Tests.Integration/TaskGenix.Tests.Integration.csproj --no-build --configuration Release --logger "trx;LogFileName=integration-test-results.trx" --results-directory TestResults + + - name: Upload Integration Test Results + uses: actions/upload-artifact@v4 + with: + name: integration-test-results + path: TestResults/*.trx + + - name: Integration Test Report + uses: dorny/test-reporter@v2 + if: always() + with: + name: Integration Test Results + path: TestResults/*.trx + reporter: dotnet-trx diff --git a/.gitignore b/.gitignore index 542ed38..5459fad 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ bld/ # Visual Studio 2015/2017 cache/options directory .vs/ +.vscode/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ @@ -365,7 +366,12 @@ FodyWeavers.xsd # Environment files .env +.idea/ + # Configuration files with sensitive data appsettings.json appsettings.*.json -appsettings.Development.json \ No newline at end of file +appsettings.Development.json +!appsettings.Test.json +TaskGenix.Server/firebase-service-account.json +/firebase-service-account.json diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..805f945 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,149 @@ +# TaskGenix Backend - Docker Setup + +This document explains how to build and deploy the TaskGenix Backend using Docker and the CI/CD pipeline. + +## 🐳 Docker Files + +### `Dockerfile.prod` + +Production-optimized Dockerfile with: + +- Multi-stage build for smaller image size +- Non-root user for security +- Health checks +- Optimized layer caching + +### `Dockerfile` (Development) + +Development Dockerfile used for local development and debugging. + +## 🚀 CI/CD Pipeline + +### Workflow Triggers + +- Push to `main` branch → Production deployment +- Push to `dev` branch → Staging deployment +- Pull requests to `main` → Build and test only +- Manual workflow dispatch + +### Required Secrets + +Add these secrets to your GitHub repository: + +```bash +# Docker Hub credentials +DOCKER_USERNAME=your-dockerhub-username +DOCKER_PASSWORD=your-dockerhub-password +``` + +### Image Tags + +- `latest` - Latest main branch build +- `dev` - Latest dev branch build +- `main-` - Specific main branch commit +- `dev-` - Specific dev branch commit +- `pr-` - Pull request builds + +## 🛠️ Local Usage + +### Build Production Image + +```bash +docker build -f Dockerfile.prod -t taskgenix-be:prod . +``` + +### Run Production Container + +```bash +docker run -d \ + --name taskgenix-be \ + -p 5009:5009 \ + -e ASPNETCORE_ENVIRONMENT=Production \ + -e JWT__SecretKey="your-secret-key" \ + -e ConnectionStrings__DefaultConnection="your-connection-string" \ + taskgenix-be:prod +``` + +### Run with Docker Compose + +```yaml +version: "3.8" +services: + app: + image: taskgenix/taskgenix-be:latest + ports: + - "5009:5009" + environment: + - ASPNETCORE_ENVIRONMENT=Production + - JWT__SecretKey=your-secret-key + - ConnectionStrings__DefaultConnection=your-connection-string + depends_on: + - db + + db: + image: postgres:16-alpine + environment: + POSTGRES_DB: TaskGenixDB + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + ports: + - "5432:5432" +``` + +## 🔧 Environment Variables + +### Required + +- `JWT__SecretKey` - JWT signing key +- `JWT__Issuer` - JWT issuer +- `JWT__Audience` - JWT audience +- `ConnectionStrings__DefaultConnection` - Database connection + +### Optional + +- `ASPNETCORE_ENVIRONMENT` - Environment (Production/Staging/Development) +- `ASPNETCORE_URLS` - Binding URLs (default: http://+:5009) + +## 📊 Health Check + +The production image includes a health check endpoint: + +```bash +curl -f http://localhost:5009/health +``` + +## 🔒 Security Features + +- Non-root user execution +- Minimal attack surface +- Security-focused base images +- Environment variable validation + +## 📈 Monitoring + +- Application logs available via `docker logs` +- Health check status via Docker +- Metrics endpoint (if implemented): `/metrics` + +## 🚨 Troubleshooting + +### Build Issues + +```bash +# Clean build +docker system prune -f +docker build --no-cache -f Dockerfile.prod -t taskgenix-be:prod . +``` + +### Runtime Issues + +```bash +# Check logs +docker logs taskgenix-be + +# Check health +docker exec taskgenix-be curl -f http://localhost:5009/health + +# Interactive shell +docker exec -it taskgenix-be /bin/bash +``` diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 0000000..cedcbe2 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,61 @@ +# Production Dockerfile for TaskGenix Backend +# Multi-stage build for optimized production image + +# Build stage +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +# Copy project files in order of dependency (for better layer caching) +COPY ["TaskGenix.Core/TaskGenix.Core.csproj", "TaskGenix.Core/"] +COPY ["TaskGenix.DAL/TaskGenix.DAL.csproj", "TaskGenix.DAL/"] +COPY ["TaskGenix.BLL/TaskGenix.BLL.csproj", "TaskGenix.BLL/"] +COPY ["TaskGenix.Server/TaskGenix.Server.csproj", "TaskGenix.Server/"] + +# Restore dependencies for all projects +RUN dotnet restore "TaskGenix.Server/TaskGenix.Server.csproj" + +# Copy source code +COPY . . + +# Build all projects in the solution +WORKDIR "/src" +RUN dotnet build "TaskGenix.Server/TaskGenix.Server.csproj" -c Release --no-restore + +# Publish stage +FROM build AS publish +WORKDIR "/src/TaskGenix.Server" +RUN dotnet publish "TaskGenix.Server.csproj" -c Release -o /app/publish \ + --no-restore \ + /p:UseAppHost=false + +# Runtime stage +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final + +# Create non-root user for security +RUN groupadd -r taskgenix && useradd -r -g taskgenix taskgenix + +# Set working directory +WORKDIR /app + +# Copy published app +COPY --from=publish /app/publish . + +# Change ownership to non-root user +RUN chown -R taskgenix:taskgenix /app + +# Switch to non-root user +USER taskgenix + +# Expose ports +EXPOSE 5009 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:5009/health || exit 1 + +# Set environment variables +ENV ASPNETCORE_ENVIRONMENT=Production +ENV ASPNETCORE_URLS=http://+:5009 + +# Entry point +ENTRYPOINT ["dotnet", "TaskGenix.Server.dll"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a5824b6 --- /dev/null +++ b/README.md @@ -0,0 +1,358 @@ +``` +████████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ███████╗███╗ ██╗██╗██╗ ██╗ +╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ ██╔════╝ ██╔════╝████╗ ██║██║╚██╗██╔╝ + ██║ ███████║███████╗█████╔╝ ██║ ███╗█████╗ ██╔██╗ ██║██║ ╚███╔╝ + ██║ ██╔══██║╚════██║██╔═██╗ ██║ ██║██╔══╝ ██║╚██╗██║██║ ██╔██╗ + ██║ ██║ ██║███████║██║ ██╗ ╚██████╔╝███████╗██║ ╚████║██║██╔╝ ██╗ + ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝ +``` + +
+ +# 🚀 TaskGenix Backend +### **Let AI Handle The Notes — You Lead The Work.** + +[![.NET](https://img.shields.io/badge/.NET-8.0-512BD4?style=for-the-badge&logo=dotnet)](https://dotnet.microsoft.com/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white)](https://www.postgresql.org/) +[![Docker](https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/) +[![JWT](https://img.shields.io/badge/JWT-000000?style=for-the-badge&logo=JSON%20web%20tokens&logoColor=white)](https://jwt.io/) + +**A modular, extensible .NET 8 backend for an AI‑assisted collaborative workspace** + +*Empowering teams with intelligent project management, smart task generation, and seamless collaboration* + +
+ +--- + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🗂️ TABLE OF CONTENTS ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +## Contents +- [🌟 Features](#-features) +- [🏗️ Architecture](#️-architecture) +- [📊 Domain Model Overview](#-domain-model-overview) +- [🛠️ Tech Stack](#️-tech-stack) +- [🚀 Quick Start](#-quick-start) +- [⚙️ Configuration](#️-configuration) +- [🔄 Running & Migration](#-running--migration) +- [📡 API Patterns](#-api-patterns) +- [📋 Example JSON](#-example-json) +- [🤖 AI Roadmap](#-ai-roadmap) +- [🧪 Testing](#-testing) +- [🤝 Contribution](#-contribution) +- [📄 License](#-license) + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🌟 FEATURES ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 🎯 Core Capabilities +- **🏢 Workspaces / Projects** - Ownership, members, roles: Owner, Member, Viewer +- **📝 Smart Task Generation** - AI-assisted task creation (`TaskGen`) with status, priority, due dates & reminder config +- **🔐 User Authentication** - JWT + refresh tokens + OTP flow + device tokens +- **💳 Subscription Management** - Quota tracking (Plans, PlanFeatures, UserPlans, UserPlanFeatures) +- **💰 Payment Integration** - Transaction scaffold (PayOS integration placeholder) +- **🔔 Notification System** - Scheduled, per-task, multi-channel ready +- **🎙️ Recording Entity** - Voice meeting capture (future transcription / summarization) + +### 🏗️ Technical Excellence +- **🧩 Clean Architecture** - Separation of layers (Core / DAL / BLL / Server / AI) +- **🔄 DTO Projection** - Via Mapster (prevents circular references) +- **✅ Centralized Validation** - Consistent API responses +- **🔒 Transaction-Safe Operations** - Unit of Work pattern + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🏗️ ARCHITECTURE ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 📁 Project Structure +``` +TaskGenix-BE/ +├── 🎯 TaskGenix.Core # Domains, DTOs, Requests, Responses, Mappings, Validators, Meta +├── 🗄️ TaskGenix.DAL # EF Core DbContext, Configurations, Migrations +├── ⚙️ TaskGenix.BLL # Business Services, Interfaces, Transaction wrappers +├── 🌐 TaskGenix.Server # ASP.NET Core Web API Host (Controllers, DI, Program) +├── 🤖 TaskGenix.AI # (Future) AI orchestration / pipelines +├── 🧪 TaskGenix.Tests.Unit +└── 🔬 TaskGenix.Tests.Integration +``` + +### 🎨 Design Patterns +- **🏛️ Repository + Unit of Work** - Clean data access abstraction +- **📦 DTO-first Controllers** - Consistent API contracts +- **🔄 Mapster Mapping Registry** - High-performance object mapping +- **🛡️ Middleware-based Exception Handling** - Centralized error management +- **✅ Custom Model Validation** - Enhanced input validation + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 📊 DOMAIN MODEL OVERVIEW ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +| 🏢 Area | 📋 Key Entities | +|---------|------------------| +| 👤 **Users & Auth** | `User`, `JwtToken`, `OtpCode`, `UserSetting` | +| 🏢 **Workspaces** | `Project`, `ProjectMember`, `Recording` | +| 📝 **Tasks** | `TaskGen`, `TaskAssignment`, `TaskReminderConfig` | +| 💳 **Plans & Billing** | `Plan`, `PlanFeature`, `UserPlan`, `UserPlanFeature`, `PaymentTransaction` | +| 🔔 **Notifications** | `Notification`, `UserDeviceToken` | +| 🤖 **AI (Future)** | `ModelPromting` placeholder | + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🛠️ TECH STACK ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 🚀 Core Technologies +- **🔷 .NET 8** / C# 12 - Latest .NET ecosystem +- **🌐 ASP.NET Core Web API** - High-performance web framework +- **🗄️ Entity Framework Core** - PostgreSQL via Npgsql +- **⚡ Mapster** - High-performance object mapping +- **🔐 JWT Authentication** - Secure token-based auth with refresh tokens + +### 🔧 Supporting Technologies +- **🔥 Firebase Admin SDK** - Push notification infrastructure +- **📚 Swagger / Swashbuckle** - API documentation +- **💾 MemoryCache** - In-memory caching +- **💳 PayOS** - Payment integration (placeholder) + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🚀 QUICK START ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 📋 Prerequisites +- ✅ **.NET 8 SDK** - Latest version +- ✅ **PostgreSQL** - Database instance +- 🔥 **(Optional) Firebase** - Service account JSON `firebase-service-account.json` +- 💳 **(Optional) PayOS** - Payment gateway credentials + +### 📥 Clone Repository +```bash +git clone https://github.com/taskgenix/TaskGenix-BE.git +cd TaskGenix-BE +``` + +### 🔨 Build & Restore +```bash +dotnet restore +dotnet build +``` + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ ⚙️ CONFIGURATION ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 📝 Configuration Setup +Add to `appsettings.Development.json` (sample): +```json +{ + "ConnectionStrings": { + "SupabaseConnection": "Host=localhost;Port=5432;Database=taskgenix;Username=postgres;Password=secret" + }, + "Jwt": { + "Issuer": "TaskGenix", + "Audience": "TaskGenix.Client", + "Key": "REPLACE_WITH_256BIT_SECRET_KEY", + "AccessTokenMinutes": 30, + "RefreshTokenDays": 7 + }, + "ApplyMigrations": true, + "PayOS": { + "ClientId": "...", + "ApiKey": "...", + "ChecksumKey": "..." + } +} +``` + +### 🔥 Firebase Setup +Place Firebase credentials file at: `TaskGenix.Server/firebase-service-account.json` (if used). + +### 🏷️ Environment Flags +| 🔑 Key | 📝 Purpose | +|--------|------------| +| `ApplyMigrations` | Auto apply EF migrations on startup (dev only) | +| `Jwt.Key` | Symmetric signing key (must be strong) | + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🔄 RUNNING & MIGRATION ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 🔄 Auto-Migration (Development) +Auto-migrate (dev): set `ApplyMigrations=true`. + +### 📊 Manual Migration +```bash +dotnet ef migrations add InitSchema -p TaskGenix.DAL -s TaskGenix.Server +dotnet ef database update -p TaskGenix.DAL -s TaskGenix.Server +``` + +### 🚀 Run API Server +```bash +dotnet run --project TaskGenix.Server +``` + +### 📚 API Documentation +- **Swagger UI**: `https://localhost:7026/swagger` +- **HTTP**: Also available on `:5009` as configured + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 📡 API PATTERNS ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### ✅ Successful Responses +All successful responses follow this pattern: +```json +{ + "message": "Operation successful", + "data": { /* payload */ } +} +``` + +### ❌ Validation Failures (HTTP 422) +```json +{ + "message": "Validation failed", + "reason": "One or more model validation errors occurred", + "errors": ["fieldName: The field fieldName is required."] +} +``` + +### 🔐 Authentication +Include JWT token in requests: +``` +Authorization: Bearer +``` + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 📋 EXAMPLE JSON ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 🏢 Project Response (trimmed) +```json +{ + "id": "5f3c...", + "name": "Workspace Alpha", + "status": "Active", + "type": "Team", + "ownerId": "9d1a...", + "members": [ + { + "user": { "id": "u1", "username": "alice" }, + "role": "Owner", + "joinedAt": "2024-01-01T00:00:00Z" + } + ], + "taskGens": [ + { + "id": "t1", + "title": "Prepare brief", + "status": "Pending", + "priority": 1 + } + ] +} +``` + +### 📝 Task Response (trimmed) +```json +{ + "id": "t1", + "title": "Prepare brief", + "status": "Pending", + "priority": "Medium", + "dueDate": "2024-12-01T12:00:00Z", + "assignments": [ + { + "assignedUser": { "id": "u1", "username": "alice" }, + "assignedAt": "2024-11-20T09:00:00Z" + } + ] +} +``` + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🤖 AI ROADMAP ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 🎯 Planned AI Capabilities +- **🎙️ Auto Task Extraction** - From meeting transcripts +- **📝 Smart Summarization** - Recordings & task clustering +- **⭐ Priority Scoring** - Using embeddings for relevance +- **📊 Quota Enforcement** - Per plan across AI features + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🧪 TESTING ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 🚀 Run All Tests +```bash +dotnet test +``` + +### 🎯 Test Coverage +- **🧪 Unit Tests** - Individual component testing +- **🔬 Integration Tests** - End-to-end scenarios +- **➕ Extensible** - Add scenarios for new services / controllers + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 🤝 CONTRIBUTION ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +### 🔄 Contribution Workflow +1. **🍴 Fork** repository +2. **🌿 Create** feature branch: `git checkout -b feat/your-feature` +3. **💻 Commit** changes: `feat: short description` +4. **🚀 Push** & open PR (reference issue if applicable) +5. **🎯 Keep** PRs focused & covered by tests + +### 📋 Coding Guidelines +- **🎯 Keep controllers thin** - Push business rules to services +- **📦 Always project to DTOs** - For outward responses +- **🔒 Use transactions** - For multi-entity mutations +- **🔄 Extend Mapster registrations** - In `TaskGenix.Core/Mappings` + +``` +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃ 📄 LICENSE ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +``` +📄 **License Information** + +Add a LICENSE file (MIT recommended) – not yet included. + +--- + +
+ +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ │ +│ ████████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ███████╗███╗ ██╗██╗██╗ ██╗ │ +│ ╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ ██╔════╝ ██╔════╝████╗ ██║██║╚██╗██╔╝ │ +│ ██║ ███████║███████╗█████╔╝ ██║ ███╗█████╗ ██╔██╗ ██║██║ ╚███╔╝ │ +│ ██║ ██╔══██║╚════██║██╔═██╗ ██║ ██║██╔══╝ ██║╚██╗██║██║ ██╔██╗ │ +│ ██║ ██║ ██║███████║██║ ██╗ ╚██████╔╝███████╗██║ ╚████║██║██╔╝ ██╗ │ +│ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝ │ +│ │ +│ 🎯 "Let AI Handle The Notes — You Lead The Work." 🎯 │ +│ │ +│ Happy building! 🚀 │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +
diff --git a/SeedData.sql b/SeedData.sql new file mode 100644 index 0000000..8d1c53b --- /dev/null +++ b/SeedData.sql @@ -0,0 +1,35 @@ +INSERT INTO "TaskGenixDb"."Users" ("Id", "Username", "AvatarUrl", "Email", "PasswordHash", "Role", "LastLogin", "EmailVerified", "IsActive", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440001', 'john_doe', 'https://avatar.com/john.jpg', 'john.doe@example.com', '123456', 'Admin', '2024-01-15 10:30:00+00', 'true', 'true', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), ('550e8400-e29b-41d4-a716-446655440002', 'jane_smith', 'https://avatar.com/jane.jpg', 'jane.smith@example.com', '123456', 'User', '2024-01-14 14:20:00+00', 'true', 'true', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), ('550e8400-e29b-41d4-a716-446655440003', 'bob_wilson', null, 'bob.wilson@example.com', '123456', 'User', '2024-01-13 16:45:00+00', 'true', 'true', '2024-01-03 11:30:00', '2024-01-13 16:45:00'), ('550e8400-e29b-41d4-a716-446655440004', 'alice_brown', 'https://avatar.com/alice.jpg', 'alice.brown@example.com', '123456', 'User', '2024-01-12 08:15:00+00', 'true', 'true', '2024-01-04 12:45:00', '2024-01-12 08:15:00'), ('550e8400-e29b-41d4-a716-446655440005', 'charlie_davis', null, 'charlie.davis@example.com', '123456', 'User', null, 'false', 'false', '2024-01-05 13:20:00', '2024-01-05 13:20:00'), ('550e8400-e29b-41d4-a716-446655440006', 'DuoG', null, 'manhduonglhp4@gmail.com', '123456', 'User', null, 'true', 'true', '2025-09-14 23:33:58', '2025-09-14 23:33:59'); + +INSERT INTO "TaskGenixDb"."Projects" ("Id", "Name", "Description", "Status", "Type", "OwnerId", "RecordingId", "DueDate", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440081', 'Website Redesign', 'Complete redesign of company website', 'Active', 'Team', '550e8400-e29b-41d4-a716-446655440001', null, '2024-03-01 23:59:59+00', '2024-01-05 09:00:00', '2024-01-15 10:30:00'), ('550e8400-e29b-41d4-a716-446655440082', 'Mobile App Development', 'Native iOS and Android app development', 'Active', 'Team', '550e8400-e29b-41d4-a716-446655440002', null, '2024-06-01 23:59:59+00', '2024-01-06 10:15:00', '2024-01-14 14:20:00'), ('550e8400-e29b-41d4-a716-446655440083', 'Marketing Campaign Q1', 'First quarter marketing campaign planning', 'NotStarted', 'Personal', '550e8400-e29b-41d4-a716-446655440003', null, '2024-03-31 23:59:59+00', '2024-01-07 11:30:00', '2024-01-13 16:45:00'), ('550e8400-e29b-41d4-a716-446655440084', 'Database Migration', 'Migrate from MySQL to PostgreSQL', 'OnHold', 'Personal', '550e8400-e29b-41d4-a716-446655440004', null, '2024-04-15 23:59:59+00', '2024-01-08 12:45:00', '2024-01-12 08:15:00'); + +INSERT INTO "TaskGenixDb"."ProjectMembers" ("Id", "Role", "ProjectId", "UserId", "JoinedAt", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440121', 'Owner', '550e8400-e29b-41d4-a716-446655440081', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 09:00:00', '2024-01-05 09:00:00', '2024-01-05 09:00:00'), ('550e8400-e29b-41d4-a716-446655440122', 'Member', '550e8400-e29b-41d4-a716-446655440081', '550e8400-e29b-41d4-a716-446655440002', '2024-01-05 09:30:00', '2024-01-05 09:30:00', '2024-01-05 09:30:00'), ('550e8400-e29b-41d4-a716-446655440123', 'Member', '550e8400-e29b-41d4-a716-446655440081', '550e8400-e29b-41d4-a716-446655440004', '2024-01-05 10:00:00', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), ('550e8400-e29b-41d4-a716-446655440124', 'Owner', '550e8400-e29b-41d4-a716-446655440082', '550e8400-e29b-41d4-a716-446655440002', '2024-01-06 10:15:00', '2024-01-06 10:15:00', '2024-01-06 10:15:00'), ('550e8400-e29b-41d4-a716-446655440125', 'Owner', '550e8400-e29b-41d4-a716-446655440083', '550e8400-e29b-41d4-a716-446655440003', '2024-01-07 11:30:00', '2024-01-07 11:30:00', '2024-01-07 11:30:00'), ('660e8400-e29b-41d4-a716-446655440843', 'Owner', '550e8400-e29b-41d4-a716-446655440084', '550e8400-e29b-41d4-a716-446655440004', '2025-09-10 22:03:24', '2025-09-10 22:03:29', '2025-09-10 22:03:30'); + +INSERT INTO "TaskGenixDb"."Plans" ("Id", "Name", "Description", "DurationInDays", "Price", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440011', 'Free Plan', 'Basic features for individual users', '365', '0.00', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440012', 'Pro Plan', 'Advanced features for professionals', '30', '29.99', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440013', 'Team Plan', 'Collaboration features for teams', '30', '99.99', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440014', 'Enterprise Plan', 'Full feature set for large organizations', '365', '299.99', '2024-01-01 08:00:00', '2024-01-01 08:00:00'); + +INSERT INTO "TaskGenixDb"."Features" ("Id", "Key", "Name", "Unit", "FeatureType", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440021', 'projects_limit', 'Maximum Projects', 'count', 'Integer', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440022', 'tasks_per_project', 'Tasks per Project', 'count', 'Integer', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440023', 'storage_limit', 'Storage Limit', 'GB', 'Decimal', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440024', 'ai_generations', 'AI Task Generations', 'count', 'Integer', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440025', 'collaboration', 'Team Collaboration', 'boolean', 'Boolean', '2024-01-01 08:00:00', '2024-01-01 08:00:00'); + +INSERT INTO "TaskGenixDb"."PlanFeatures" ("Id", "QuotaValue", "IsUnlimited", "PlanId", "FeatureId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440031', '3', 'false', '550e8400-e29b-41d4-a716-446655440011', '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440032', '10', 'false', '550e8400-e29b-41d4-a716-446655440011', '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440033', '1', 'false', '550e8400-e29b-41d4-a716-446655440011', '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440034', '50', 'false', '550e8400-e29b-41d4-a716-446655440011', '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440035', '25', 'false', '550e8400-e29b-41d4-a716-446655440012', '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440036', '100', 'false', '550e8400-e29b-41d4-a716-446655440012', '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440037', '10', 'false', '550e8400-e29b-41d4-a716-446655440012', '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440038', '500', 'false', '550e8400-e29b-41d4-a716-446655440012', '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440039', null, 'true', '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440040', null, 'true', '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440041', '100', 'false', '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440042', '2000', 'false', '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), ('550e8400-e29b-41d4-a716-446655440043', '1', 'false', '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440025', '2024-01-01 08:00:00', '2024-01-01 08:00:00'); + +INSERT INTO "TaskGenixDb"."UserSettings" ("Id", "ReceiveEmailNotifications", "DarkModeEnabled", "Language", "UserId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440171', 'true', 'false', 'en-US', '550e8400-e29b-41d4-a716-446655440001', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), ('550e8400-e29b-41d4-a716-446655440172', 'true', 'true', 'en-US', '550e8400-e29b-41d4-a716-446655440002', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), ('550e8400-e29b-41d4-a716-446655440173', 'false', 'false', 'en-US', '550e8400-e29b-41d4-a716-446655440003', '2024-01-03 11:30:00', '2024-01-13 16:45:00'), ('550e8400-e29b-41d4-a716-446655440174', 'true', 'true', 'es-ES', '550e8400-e29b-41d4-a716-446655440004', '2024-01-04 12:45:00', '2024-01-12 08:15:00'), ('550e8400-e29b-41d4-a716-446655440175', 'false', 'false', 'en-US', '550e8400-e29b-41d4-a716-446655440005', '2024-01-05 13:20:00', '2024-01-05 13:20:00'); + +INSERT INTO "TaskGenixDb"."UserPlans" ("Id", "Name", "Price", "UserId", "PlanId", "StartDate", "EndDate", "IsActive", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440051', 'Pro Plan', '29.99', '550e8400-e29b-41d4-a716-446655440001', '550e8400-e29b-41d4-a716-446655440012', '2024-01-01 00:00:00+00', '2024-02-01 00:00:00+00', 'true', '2024-01-01 09:00:00', '2024-01-01 09:00:00'), ('550e8400-e29b-41d4-a716-446655440052', 'Free Plan', '0.00', '550e8400-e29b-41d4-a716-446655440002', '550e8400-e29b-41d4-a716-446655440011', '2024-01-02 00:00:00+00', '2025-01-02 00:00:00+00', 'true', '2024-01-02 10:15:00', '2024-01-02 10:15:00'), ('550e8400-e29b-41d4-a716-446655440053', 'Team Plan', '99.99', '550e8400-e29b-41d4-a716-446655440003', '550e8400-e29b-41d4-a716-446655440013', '2024-01-03 00:00:00+00', '2024-02-03 00:00:00+00', 'true', '2024-01-03 11:30:00', '2024-01-03 11:30:00'), ('550e8400-e29b-41d4-a716-446655440054', 'Enterprise Plan', '299.99', '550e8400-e29b-41d4-a716-446655440004', '550e8400-e29b-41d4-a716-446655440014', '2024-01-04 00:00:00+00', '2025-01-04 00:00:00+00', 'true', '2024-01-04 12:45:00', '2024-01-04 12:45:00'); + +INSERT INTO "TaskGenixDb"."UserPlanFeatures" ("Id", "QuotaTotal", "QuotaUsed", "QuotaRemaining", "IsUnlimited", "UserPlanId", "FeatureId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440061', '25.00', '5.00', '20.00', 'false', '550e8400-e29b-41d4-a716-446655440051', '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), ('550e8400-e29b-41d4-a716-446655440062', '100.00', '45.00', '55.00', 'false', '550e8400-e29b-41d4-a716-446655440051', '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), ('550e8400-e29b-41d4-a716-446655440063', '3.00', '2.00', '1.00', 'false', '550e8400-e29b-41d4-a716-446655440052', '550e8400-e29b-41d4-a716-446655440021', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), ('550e8400-e29b-41d4-a716-446655440064', '10.00', '8.00', '2.00', 'false', '550e8400-e29b-41d4-a716-446655440052', '550e8400-e29b-41d4-a716-446655440022', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), ('550e8400-e29b-41d4-a716-446655440065', '0.00', '15.00', '0.00', 'true', '550e8400-e29b-41d4-a716-446655440053', '550e8400-e29b-41d4-a716-446655440021', '2024-01-03 11:30:00', '2024-01-13 16:45:00'), ('550e8400-e29b-41d4-a716-446655440066', '0.00', '75.00', '0.00', 'true', '550e8400-e29b-41d4-a716-446655440053', '550e8400-e29b-41d4-a716-446655440022', '2024-01-03 11:30:00', '2024-01-13 16:45:00'); + +INSERT INTO "TaskGenixDb"."UserDeviceTokens" ("Id", "FCMToken", "DeviceId", "DeviceName", "Platform", "UserId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440181', 'dGhpc19pc19hX21vY2tfZmNtX3Rva2VuXzEyMzQ1', 'iPhone_12_Pro_001', 'John''s iPhone', 'iOS', '550e8400-e29b-41d4-a716-446655440001', '2024-01-01 09:00:00+00', '2024-01-15 10:30:00+00'), ('550e8400-e29b-41d4-a716-446655440182', 'YW5vdGhlcl9tb2NrX2ZjbV90b2tlbl9hYmNkZWY=', 'Pixel_6_002', 'Jane''s Pixel', 'Android', '550e8400-e29b-41d4-a716-446655440002', '2024-01-02 10:15:00+00', '2024-01-14 14:20:00+00'), ('550e8400-e29b-41d4-a716-446655440183', 'bW9ja19mY21fdG9rZW5fZm9yX2JvYl93aWxzb24=', 'Galaxy_S21_003', 'Bob''s Galaxy', 'Android', '550e8400-e29b-41d4-a716-446655440003', '2024-01-03 11:30:00+00', '2024-01-13 16:45:00+00'), ('550e8400-e29b-41d4-a716-446655440184', 'YWxpY2VfYnJvd25fZmNtX3Rva2VuXzc4OTEwMTExMg==', 'MacBook_Pro_004', 'Alice''s MacBook', 'macOS', '550e8400-e29b-41d4-a716-446655440004', '2024-01-04 12:45:00+00', '2024-01-12 08:15:00+00'), ('550e8400-e29b-41d4-a716-446655440185', 'Y2hhcmxpZV9kYXZpc19mY21fdG9rZW5fMTMxNDE1MTY=', 'iPad_Air_005', 'Charlie''s iPad', 'iOS', '550e8400-e29b-41d4-a716-446655440005', '2024-01-05 13:20:00+00', '2024-01-05 13:20:00+00'); + +INSERT INTO "TaskGenixDb"."TaskGens" ("Id", "Title", "Description", "Status", "Priority", "ProjectId", "DueDate", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440101', 'Create Homepage Wireframes', 'Design wireframes for the new homepage layout', 'InProgress', 'High', '550e8400-e29b-41d4-a716-446655440081', '2024-01-25 17:00:00+00', '2024-01-05 09:30:00', '2024-01-15 11:00:00'), ('550e8400-e29b-41d4-a716-446655440102', 'Implement User Authentication', 'Build login and registration functionality', 'Pending', 'High', '550e8400-e29b-41d4-a716-446655440081', '2024-02-01 17:00:00+00', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), ('550e8400-e29b-41d4-a716-446655440103', 'Setup Database Schema', 'Create and configure database tables', 'Completed', 'Medium', '550e8400-e29b-41d4-a716-446655440081', '2024-01-20 17:00:00+00', '2024-01-05 10:30:00', '2024-01-20 16:30:00'), ('550e8400-e29b-41d4-a716-446655440104', 'Mobile App Market Research', 'Research competitor apps and market trends', 'InProgress', 'Medium', '550e8400-e29b-41d4-a716-446655440082', '2024-02-15 17:00:00+00', '2024-01-06 11:00:00', '2024-01-14 15:00:00'), ('550e8400-e29b-41d4-a716-446655440105', 'Create Social Media Content', 'Develop content for Q1 social media campaign', 'Completed', 'Low', '550e8400-e29b-41d4-a716-446655440083', '2024-02-28 17:00:00+00', '2024-01-07 12:00:00', '2024-02-25 14:00:00'), ('550e8400-e29b-41d4-a716-446655440106', 'API Documentation', 'Create comprehensive API documentation', 'Pending', 'Medium', '550e8400-e29b-41d4-a716-446655440081', '2024-02-10 17:00:00+00', '2024-01-16 09:00:00', '2024-01-16 09:00:00'), ('550e8400-e29b-41d4-a716-446655440107', 'UI/UX Testing', 'Conduct usability testing for mobile app', 'InProgress', 'High', '550e8400-e29b-41d4-a716-446655440082', '2024-02-20 17:00:00+00', '2024-01-17 10:00:00', '2024-01-20 15:00:00'), ('550e8400-e29b-41d4-a716-446655440108', 'Performance Optimization', 'Optimize database queries and indexing', 'Pending', 'Low', '550e8400-e29b-41d4-a716-446655440084', '2024-03-15 17:00:00+00', '2024-01-18 11:00:00', '2024-01-18 11:00:00'); + +INSERT INTO "TaskGenixDb"."TaskReminderConfigs" ("Id", "TaskGenId", "EnableReminders", "ReminderFrequencyInDays", "StartRemindingDaysBeforeDue", "ReminderIntervalInHours", "PreferredReminderHour", "EscalateReminderPriority", "PreferredChannel", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440131', '550e8400-e29b-41d4-a716-446655440101', 'true', '1', '3', '24', '9', 'true', 'Email', '2024-01-05 09:30:00', '2024-01-05 09:30:00'), ('550e8400-e29b-41d4-a716-446655440132', '550e8400-e29b-41d4-a716-446655440102', 'true', '2', '5', '48', '14', 'false', 'Push', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), ('550e8400-e29b-41d4-a716-446655440133', '550e8400-e29b-41d4-a716-446655440104', 'true', '1', '2', '12', '10', 'true', 'Email', '2024-01-06 11:00:00', '2024-01-06 11:00:00'); + +INSERT INTO "TaskGenixDb"."TaskAssignments" ("Id", "TaskId", "UserId", "AssignedAt") VALUES ('550e8400-e29b-41d4-a716-446655440111', '550e8400-e29b-41d4-a716-446655440101', '550e8400-e29b-41d4-a716-446655440002', '2024-01-05 09:45:00'), ('550e8400-e29b-41d4-a716-446655440112', '550e8400-e29b-41d4-a716-446655440102', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 10:15:00'), ('550e8400-e29b-41d4-a716-446655440113', '550e8400-e29b-41d4-a716-446655440103', '550e8400-e29b-41d4-a716-446655440004', '2024-01-05 10:45:00'), ('550e8400-e29b-41d4-a716-446655440114', '550e8400-e29b-41d4-a716-446655440104', '550e8400-e29b-41d4-a716-446655440003', '2024-01-06 11:15:00'), ('550e8400-e29b-41d4-a716-446655440115', '550e8400-e29b-41d4-a716-446655440105', '550e8400-e29b-41d4-a716-446655440002', '2024-01-07 12:30:00'), ('550e8400-e29b-41d4-a716-446655440116', '550e8400-e29b-41d4-a716-446655440106', '550e8400-e29b-41d4-a716-446655440001', '2024-01-16 09:15:00'), ('550e8400-e29b-41d4-a716-446655440117', '550e8400-e29b-41d4-a716-446655440107', '550e8400-e29b-41d4-a716-446655440002', '2024-01-17 10:15:00'), ('550e8400-e29b-41d4-a716-446655440118', '550e8400-e29b-41d4-a716-446655440108', '550e8400-e29b-41d4-a716-446655440004', '2024-01-18 11:15:00'); + +INSERT INTO "TaskGenixDb"."ResourceLinks" ("Id", "Title", "Url", "Description", "Type", "TaskGenId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440161', 'Figma Design System', 'https://figma.com/design-system-templates', 'Design system templates and components', 'Design', '550e8400-e29b-41d4-a716-446655440101', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), ('550e8400-e29b-41d4-a716-446655440162', 'Authentication Best Practices', 'https://auth0.com/docs/best-practices', 'Comprehensive guide on authentication implementation', 'Documentation', '550e8400-e29b-41d4-a716-446655440102', '2024-01-05 10:30:00', '2024-01-05 10:30:00'), ('550e8400-e29b-41d4-a716-446655440163', 'PostgreSQL Migration Guide', 'https://postgresql.org/docs/migration', 'Step-by-step database migration documentation', 'Documentation', '550e8400-e29b-41d4-a716-446655440103', '2024-01-05 11:00:00', '2024-01-05 11:00:00'), ('550e8400-e29b-41d4-a716-446655440164', 'App Store Guidelines', 'https://developer.apple.com/app-store/guidelines/', 'Apple App Store review guidelines', 'Reference', '550e8400-e29b-41d4-a716-446655440104', '2024-01-06 11:30:00', '2024-01-06 11:30:00'), ('550e8400-e29b-41d4-a716-446655440165', 'Social Media Analytics Tool', 'https://analytics.socialmedia.com', 'Tool for tracking social media performance metrics', 'Tool', '550e8400-e29b-41d4-a716-446655440105', '2024-01-07 13:00:00', '2024-01-07 13:00:00'); + +INSERT INTO "TaskGenixDb"."Recordings" ("Id", "Title", "FileUrl", "Duration", "ProjectId", "UserId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440091', 'Project Kickoff Meeting', 'https://storage.example.com/recordings/kickoff_001.mp3', '01:45:30', '550e8400-e29b-41d4-a716-446655440081', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 09:30:00+00', '2024-01-05 09:30:00+00'), ('550e8400-e29b-41d4-a716-446655440093', 'Marketing Strategy Discussion', 'https://storage.example.com/recordings/marketing_003.mp3', '02:15:45', '550e8400-e29b-41d4-a716-446655440083', '550e8400-e29b-41d4-a716-446655440003', '2024-01-08 16:00:00+00', '2024-01-08 16:00:00+00'); + +INSERT INTO "TaskGenixDb"."PaymentTransactions" ("Id", "Amount", "Currency", "TransactionDate", "PaymentMethod", "PaymentGateway", "ExternalTransactionId", "Status", "UserId", "UserPlanId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440071', '29.99', 'USD', '2024-01-01 09:00:00', 'Credit Card', 'Stripe', 'txn_1234567890', 'Completed', '550e8400-e29b-41d4-a716-446655440001', '550e8400-e29b-41d4-a716-446655440051', '2024-01-01 09:00:00', '2024-01-01 09:00:00'), ('550e8400-e29b-41d4-a716-446655440072', '99.99', 'USD', '2024-01-03 11:30:00', 'PayPal', 'PayPal', 'PAYID-ABC123DEF', 'Completed', '550e8400-e29b-41d4-a716-446655440003', '550e8400-e29b-41d4-a716-446655440053', '2024-01-03 11:30:00', '2024-01-03 11:30:00'), ('550e8400-e29b-41d4-a716-446655440073', '299.99', 'USD', '2024-01-04 12:45:00', 'Bank Transfer', 'Stripe', 'txn_0987654321', 'Completed', '550e8400-e29b-41d4-a716-446655440004', '550e8400-e29b-41d4-a716-446655440054', '2024-01-04 12:45:00', '2024-01-04 12:45:00'); + +INSERT INTO "TaskGenixDb"."Notifications" ("Id", "Title", "Message", "Type", "IsSent", "IsRead", "TaskGenId", "SequenceNumber", "ReminderPattern", "ReminderSeriesId", "ScheduledTime", "SentAt", "DeliveryStatus", "DeliveryChannel", "UserId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440141', 'Task Due Soon', 'Your task "Create Homepage Wireframes" is due in 2 days', 'DueDate', 'true', 'false', '550e8400-e29b-41d4-a716-446655440101', '1', 'Daily', '550e8400-e29b-41d4-a716-446655440151', '2024-01-23 09:00:00+00', '2024-01-23 09:00:00+00', 'Sent', 'Email', '550e8400-e29b-41d4-a716-446655440002', '2024-01-23 08:55:00', '2024-01-23 09:00:00'), ('550e8400-e29b-41d4-a716-446655440142', 'New Task Assigned', 'You have been assigned a new task: "Implement User Authentication"', 'TaskAssigned', 'true', 'true', '550e8400-e29b-41d4-a716-446655440102', null, null, null, null, '2024-01-05 10:15:00+00', 'Sent', 'PushNotification', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 10:15:00', '2024-01-05 10:15:00'), ('550e8400-e29b-41d4-a716-446655440143', 'Task Completed', 'Task "Setup Database Schema" has been marked as completed', 'TaskCompleted', 'true', 'false', '550e8400-e29b-41d4-a716-446655440103', null, null, null, null, '2024-01-20 16:30:00+00', 'Sent', 'Email', '550e8400-e29b-41d4-a716-446655440001', '2024-01-20 16:30:00', '2024-01-20 16:30:00'), ('550e8400-e29b-41d4-a716-446655440144', 'Welcome to TaskGenix', 'Welcome to TaskGenix! Get started by creating your first project.', 'Information', 'true', 'true', null, null, null, null, null, '2024-01-01 09:00:00+00', 'Delivered', 'Email', '550e8400-e29b-41d4-a716-446655440001', '2024-01-01 09:00:00', '2024-01-15 10:30:00'); + +INSERT INTO "TaskGenixDb"."Attachments" ("Id", "FileName", "FileType", "FileSize", "Data", "TaskGenId", "CreatedAt", "UpdatedAt") VALUES ('550e8400-e29b-41d4-a716-446655440151', 'wireframe_homepage_v1.pdf', 'application/pdf', '2048576', '\x255044462d312e340a25d0d4c5d80a34203020206f626a0a3c3c0a2f4c656e6774682035203020520a2f46696c7465722f466c6174654465636f64650a3e3e0a73747265616d0a', '550e8400-e29b-41d4-a716-446655440101', '2024-01-10 14:30:00', '2024-01-10 14:30:00'), ('550e8400-e29b-41d4-a716-446655440152', 'auth_requirements.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '524288', '\x504b030414000600080000002100b7b94f4d61010000e7040000130008025b436f6e74656e745f54797065735d2e786d6c20a2040228a0000200000000000000000000', '550e8400-e29b-41d4-a716-446655440102', '2024-01-05 11:00:00', '2024-01-05 11:00:00'), ('550e8400-e29b-41d4-a716-446655440153', 'database_schema.sql', 'text/plain', '10240', '\x435245415445205441424c4520757365727320280a20206964205345524941204e4f54204e554c4c2050524956415259204b45592c0a20206e616d6520564152434841522835302920554e495155452c0a2020656d61696c20564152434841522831303029204e4f54204e554c4c0a293b', '550e8400-e29b-41d4-a716-446655440103', '2024-01-08 16:45:00', '2024-01-08 16:45:00'); \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Background/CleanupBackgroundService.cs b/TaskGenix.BLL/Services/Background/CleanupBackgroundService.cs new file mode 100644 index 0000000..ddb8cc3 --- /dev/null +++ b/TaskGenix.BLL/Services/Background/CleanupBackgroundService.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; + +namespace TaskGenix.BLL.Services.Background; + +public class CleanupBackgroundService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + private readonly ILogger _logger; + private readonly TimeSpan _cleanupInterval = TimeSpan.FromHours(24); // Run every 24 hours + + public CleanupBackgroundService( + IServiceProvider serviceProvider, + ILogger logger) + { + _serviceProvider = serviceProvider; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try + { + _logger.LogInformation("Starting cleanup process"); + + using var scope = _serviceProvider.CreateScope(); + + var jwtTokenService = scope.ServiceProvider.GetRequiredService(); + var otpService = scope.ServiceProvider.GetRequiredService(); + + // Clean up expired JWT tokens + await jwtTokenService.CleanupExpiredTokensAsync(); + + // Clean up expired OTP codes + await otpService.CleanupExpiredOtpsAsync(); + + _logger.LogInformation("Cleanup process completed successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred during cleanup process"); + } + + // Wait for the next cleanup interval + await Task.Delay(_cleanupInterval, stoppingToken); + } + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/AIGenService.cs b/TaskGenix.BLL/Services/Implements/AIGenService.cs new file mode 100644 index 0000000..ebbbd62 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/AIGenService.cs @@ -0,0 +1,412 @@ +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.PromptTemplates.Handlebars; +using Microsoft.SemanticKernel.Services; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using TaskGenix.BLL.Services; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.AIModel; +using TaskGenix.Core.Requests.ResourceLink; +using TaskGenix.Core.Requests.TaskGen; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements +{ + public class AIGenService : BaseService, IAIGenService + { + private readonly Kernel _kernel; + private readonly IServiceProvider _serviceProvider; + private readonly JsonSerializerOptions _jsonOptions; + + public AIGenService( + IUnitOfWork unitOfWork, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper, + Kernel kernel, + IServiceProvider serviceProvider + ) : base(unitOfWork, logger, httpContextAccessor, mapper) + { + _kernel = kernel; + _serviceProvider = serviceProvider; + _jsonOptions = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + } + + public async Task SummarizeTranscriptAsync(CreateSummaryRequest request) + { + try + { + _logger.LogInformation("Summarizing transcript, length: {Length}", request.Transcript.Length); + + // Get the prompt template + var promptTemplate = ModelPromting.GetSummarizePromptTemplate(); + + // Create the function with Handlebars for summarize + var handlebarsFactory = new HandlebarsPromptTemplateFactory(); + var functionConfig = new PromptTemplateConfig + { + Template = promptTemplate, + TemplateFormat = "handlebars" + }; + + // Add execution settings with the correct service ID + functionConfig.ExecutionSettings = new Dictionary + { + ["gemini-summarize"] = new PromptExecutionSettings() + }; + + var summarizeFunction = _kernel.CreateFunctionFromPrompt(functionConfig, handlebarsFactory); + + // Prepare the variables to fill the template + var arguments = new KernelArguments + { + ["transcript"] = request.Transcript, + ["locale"] = request.Locale ?? "en-US" + }; + + // Execute the function and get the result + var result = await _kernel.InvokeAsync(summarizeFunction, arguments); + string summary = result.GetValue() ?? string.Empty; + + _logger.LogInformation("Successfully generated summary, length: {Length}", summary.Length); + return summary; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error summarizing transcript"); + throw; + } + } + + public async Task> GenerateTasksFromSummaryAsync(GenerateTasksRequest request) + { + try + { + _logger.LogInformation("Generating tasks from summary, length: {Length}", request.SummaryText.Length); + + // Get the prompt template + var promptTemplate = ModelPromting.GetGenerateTaskWithResourcesPromptTemplate(); + + // Create the function with Handlebars + var handlebarsFactory = new HandlebarsPromptTemplateFactory(); + var functionConfig = new PromptTemplateConfig + { + Template = promptTemplate, + TemplateFormat = "handlebars" + }; + + // Add execution settings with the correct service ID for task generation + functionConfig.ExecutionSettings = new Dictionary + { + ["gemini-taskgen"] = new PromptExecutionSettings() + }; + + var generateTasksFunction = _kernel.CreateFunctionFromPrompt(functionConfig, handlebarsFactory); + + // Prepare the variables to fill the template + var arguments = new KernelArguments + { + ["summary"] = request.SummaryText, + ["locale"] = request.Locale ?? "en-US" + }; + + // Execute the function and get the result + var result = await _kernel.InvokeAsync(generateTasksFunction, arguments); + string jsonResult = result.GetValue() ?? "[]"; + + // Log the raw JSON result for debugging + _logger.LogInformation("Raw JSON result: {JsonResult}", jsonResult); + + // Clean the JSON result from any markdown formatting + jsonResult = CleanJsonFromMarkdownFormatting(jsonResult); + + // Parse the JSON result and convert to CreateTaskRequest objects + var taskRequests = ParseJsonToTaskRequests(jsonResult); + + _logger.LogInformation("Successfully generated {Count} tasks", taskRequests.Count); + return taskRequests; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating tasks from summary"); + throw; + } + } + + public async Task<(string Summary, List Tasks)> ProcessTranscriptToTasksAsync(CreateSummaryRequest request) + { + // First generate the summary + var summary = await SummarizeTranscriptAsync(request); + + // Then generate tasks from the summary + var tasksRequest = new GenerateTasksRequest + { + SummaryText = summary, + Locale = request.Locale + }; + var tasks = await GenerateTasksFromSummaryAsync(tasksRequest); + + return (summary, tasks); + } + + #region Helper Methods + private List ParseJsonToTaskRequests(string jsonResult) + { + try + { + _logger.LogInformation("Attempting to deserialize JSON result, length: {Length}", jsonResult?.Length ?? 0); + + // Safety check - provide default if null + jsonResult = jsonResult ?? "[]"; + + // Enhanced JSON options with extra settings for handling Vietnamese characters + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // Help with Vietnamese characters + AllowTrailingCommas = true, // More forgiving JSON parsing + ReadCommentHandling = JsonCommentHandling.Skip // Skip comments if any + }; + + // Define a private class to match the AI model's output structure + var aiTasks = JsonSerializer.Deserialize>(jsonResult, options) ?? new List(); + + _logger.LogInformation("Successfully deserialized {Count} tasks", aiTasks.Count); + + // Convert to CreateTaskRequest objects + var taskRequests = aiTasks.Select(t => new CreateTaskRequest + { + Title = t.Title, + Description = t.Description, + DueDate = ParseDueDate(t.DueDate), + Status = MapStatus(t.Status), + Priority = MapPriority(t.Priority), + ResourceLinks = t.ResourceLinks?.Select(r => new ResourceLinkRequest + { + Title = r.Title, + Url = r.Url, + Description = r.Description, + Type = MapResourceType(r.Type) + }).ToList() + }).ToList(); + + return taskRequests; + } + catch (JsonException ex) + { + _logger.LogError(ex, "Error parsing AI response: {JsonResult}", jsonResult); + + // Try manual fallback parsing for basic tasks if the JSON is invalid + _logger.LogInformation("Attempting alternative parsing approach..."); + try + { + // Try to fix common JSON issues (missing quotes, invalid escaping) + var fixedJson = AttemptToFixCommonJsonIssues(jsonResult); + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; + + var aiTasks = JsonSerializer.Deserialize>(fixedJson, options); + if (aiTasks != null) + { + _logger.LogInformation("Alternative parsing succeeded, recovered {Count} tasks", aiTasks.Count); + return aiTasks.Select(t => new CreateTaskRequest + { + Title = t.Title, + Description = t.Description, + DueDate = ParseDueDate(t.DueDate), + Status = MapStatus(t.Status), + Priority = MapPriority(t.Priority) + }).ToList(); + } + } + catch (Exception fallbackEx) + { + _logger.LogError(fallbackEx, "Fallback parsing also failed"); + } + + return new List(); + } + } + + private DateTime ParseDueDate(string dueDateStr) + { + if (DateTime.TryParse(dueDateStr, out DateTime dueDate)) + { + return dueDate; + } + + // Default to 1 week from now if parsing fails + return DateTime.Today.AddDays(7); + } + + private TaskGenStatus MapStatus(string status) + { + return status?.ToLower() switch + { + "in progress" or "inprogress" or "in-progress" => TaskGenStatus.InProgress, + "completed" or "done" => TaskGenStatus.Completed, + _ => TaskGenStatus.Pending + }; + } + + private TaskGenPriority MapPriority(string priority) + { + return priority?.ToLower() switch + { + "critical" or "urgent" => TaskGenPriority.Critical, + "high" => TaskGenPriority.High, + "medium" => TaskGenPriority.Medium, + _ => TaskGenPriority.Low + }; + } + + private ResourceType MapResourceType(string type) + { + return type?.ToLower() switch + { + "article" => ResourceType.Article, + "documentation" or "docs" => ResourceType.Documentation, + "video" => ResourceType.Video, + "tutorial" => ResourceType.Tutorial, + "blog" => ResourceType.Blog, + "news" => ResourceType.News, + "design" => ResourceType.Design, + "reference" => ResourceType.Reference, + "tool" => ResourceType.Tool, + _ => ResourceType.Other + }; + } + + /// + /// Cleans JSON string from any markdown code block formatting + /// + private string CleanJsonFromMarkdownFormatting(string input) + { + if (string.IsNullOrEmpty(input)) + return "[]"; // Return empty array as default + + // Log the first few characters for debugging + _logger.LogInformation("First 10 characters of input: {Start}", + input.Length >= 10 ? input.Substring(0, 10) : input); + + // Remove markdown code block formatting if present + input = input.Trim(); + + // Remove markdown code block with language specifier - ```json + if (input.StartsWith("```json") || input.StartsWith("```JSON")) + { + input = input.Substring(7); // Remove ```json + } + // Remove simple markdown code block - ``` + else if (input.StartsWith("```")) + { + input = input.Substring(3); // Remove ``` + } + // Remove backtick at the start if present + else if (input.StartsWith("`")) + { + input = input.Substring(1); // Remove ` + } + + // Remove end of code block if present + if (input.EndsWith("```")) + { + input = input.Substring(0, input.Length - 3); + } + // Remove backtick at the end if present + else if (input.EndsWith("`")) + { + input = input.Substring(0, input.Length - 1); + } + + // Trim again to remove any whitespace + input = input.Trim(); + + _logger.LogInformation("Cleaned JSON: {CleanedJson}", + input.Length > 50 ? input.Substring(0, 50) + "..." : input); + + return input; + } + + + private string AttemptToFixCommonJsonIssues(string json) + { + if (string.IsNullOrEmpty(json)) + return "[]"; + + // Make sure it starts with [ for an array + if (!json.TrimStart().StartsWith("[")) + { + json = "[" + json; + } + + // Make sure it ends with ] for an array + if (!json.TrimEnd().EndsWith("]")) + { + json = json + "]"; + } + + // Replace escaped Unicode Vietnamese characters with their actual representation + // This helps with common escaping issues in Vietnamese text + json = System.Text.RegularExpressions.Regex.Replace( + json, + @"\\u([0-9a-fA-F]{4})", + match => char.ConvertFromUtf32(int.Parse(match.Groups[1].Value, System.Globalization.NumberStyles.HexNumber)) + ); + + // Replace literal \n with actual newlines + json = json.Replace("\\n", "\n"); + + // Fix missing quotes around property names + json = System.Text.RegularExpressions.Regex.Replace( + json, + @"([{,])\s*([a-zA-Z0-9_]+)\s*:", + "$1\"$2\":" + ); + + _logger.LogInformation("Fixed JSON: {FixedJson}", + json.Length > 50 ? json.Substring(0, 50) + "..." : json); + + return json; + } + + #endregion + } + + #region AI Response Models + public class AIGeneratedTask + { + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string DueDate { get; set; } = string.Empty; + public string Priority { get; set; } = "Medium"; + public string Status { get; set; } = "Pending"; + public List? ResourceLinks { get; set; } + } + + public class AIResourceLink + { + public string Title { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string Type { get; set; } = "Article"; + } + + #endregion +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/AuthService.cs b/TaskGenix.BLL/Services/Implements/AuthService.cs new file mode 100644 index 0000000..b3b4e61 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/AuthService.cs @@ -0,0 +1,436 @@ +using System.Globalization; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using System.Security.Claims; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Constants; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.Core.Requests.Auth; +using TaskGenix.Core.Responses.Auth; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements; + +public class AuthService( + IUnitOfWork unitOfWork, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper, + IOtpService otpService, + IEmailService emailService, + IJwtService jwtService, + IJwtTokenService jwtTokenService, + IConfiguration configuration +) + : BaseService(unitOfWork, logger, httpContextAccessor, mapper), IAuthService +{ + private readonly int AccessTokenExpirationHours = 1; + + public async Task LoginAsync(LoginRequest request) + { + var user = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Email == request.Email && u.IsActive); + + if (user == null) + throw new UnauthorizedException(CommonExceptionMessage.InvalidCredentials); + + if (!VerifyPassword(request.Password, user.PasswordHash)) + throw new UnauthorizedException(CommonExceptionMessage.InvalidCredentials); + + // Update last login + user.LastLogin = DateTime.UtcNow; + await _unitOfWork.GetRepository().Update(user); + await _unitOfWork.CommitAsync(); + + emailService.SendLoginSuccessEmailAsync(user.Email); + + // Generate tokens + var token = jwtService.GenerateToken(user); + var refreshToken = jwtService.GenerateRefreshToken(user); + + // Save refresh token + await jwtTokenService.SaveTokenAsync(user, token, refreshToken); + + return new LoginResponse + { + Token = new TokenResponse + { + AccessToken = token, + RefreshToken = refreshToken, + ExpirationDate = DateTime.UtcNow.AddHours(AccessTokenExpirationHours) + .ToString(CultureInfo.InvariantCulture), + RefreshExpirationDate = DateTime.UtcNow.AddDays(182).ToString(CultureInfo.InvariantCulture) + }, + Account = new UserInfo + { + Id = user.Id, + Username = user.Username, + Email = user.Email, + Role = user.Role.ToString() + } + }; + } + + public async Task RegisterAsync(RegisterRequest request) + { + await _unitOfWork.ExecuteInTransactionAsync(async () => + { + // Check if user already exists + var existingUser = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Email == request.Email); + + if (existingUser != null) throw new ConflictException(CommonExceptionMessage.EmailAlreadyInUse); + + var passwordHash = HashPassword(request.Password); + + var user = new User + { + Username = request.Username, + Email = request.Email, + PasswordHash = passwordHash, + Role = UserRole.User, + EmailVerified = false, + IsActive = true + }; + + user.AvatarUrl = $"https://api.dicebear.com/9.x/adventurer/png?seed={user.Username}"; + + await _unitOfWork.GetRepository().Insert(user); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("User registered successfully: {Email}", request.Email); + }); + } + + public async Task LogoutAsync() + { + try + { + var userId = GetCurrentUserId(); + await jwtTokenService.InvalidateAllUserTokensAsync(userId); + _logger.LogInformation("User logged out: {UserId}", userId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Logout failed"); + throw; + } + } + + public async Task RefreshTokenAsync(string refreshToken) + { + // Validate refresh token and get user + var user = await jwtTokenService.ValidateRefreshTokenAsync(refreshToken); + + // Generate new tokens + var newToken = jwtService.GenerateToken(user); + var newRefreshToken = jwtService.GenerateRefreshToken(user); + + // Save new refresh token and invalidate old one + await jwtTokenService.SaveTokenAsync(user, newToken, newRefreshToken); + await jwtTokenService.InvalidateTokenAsync(refreshToken); + + return new LoginResponse + { + Token = new TokenResponse + { + AccessToken = newToken, + RefreshToken = newRefreshToken, + ExpirationDate = DateTime.UtcNow.AddHours(AccessTokenExpirationHours) + .ToString(CultureInfo.InvariantCulture), + RefreshExpirationDate = DateTime.UtcNow.AddDays(182).ToString(CultureInfo.InvariantCulture) + }, + Account = new UserInfo + { + Id = user.Id, + Username = user.Username, + Email = user.Email, + Role = user.Role.ToString() + } + }; + } + + public async Task RequestOtpAsync(string email) + { + if (email.Equals("testreviewer@taskgenix.app")) + { + await SendOtpEmailAsync(email, "000000"); + _logger.LogInformation("Tester Trigger OTP Login"); + } + else + { + // Check OTP quota using the new OTP service + if (!await otpService.CanRequestOtpAsync(email)) + throw new TooManyRequestsException(CommonExceptionMessage.OtpResendLimitExceeded); + + // Generate and save OTP + var otp = await otpService.GenerateOtpAsync(email, OtpPurpose.Login); + + // Send OTP via email + await SendOtpEmailAsync(email, otp); + _logger.LogInformation("OTP sent to email: {Email}", email); + } + } + + public async Task VerifyOtpAsync(VerifyOtpRequest request) + { + // Bypass OTP check for specific tester email + if (request.Email.Equals("testreviewer@taskgenix.app") && request.Code == "000000") + { + + var existingUser = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Email == request.Email && u.IsActive); + + // Update last login + existingUser.LastLogin = DateTime.UtcNow; + await _unitOfWork.GetRepository().Update(existingUser); + await _unitOfWork.CommitAsync(); + + // Generate tokens + var token = jwtService.GenerateToken(existingUser); + var refreshToken = jwtService.GenerateRefreshToken(existingUser); + + return new LoginResponse + { + Token = new TokenResponse + { + AccessToken = token, + RefreshToken = refreshToken, + ExpirationDate = DateTime.UtcNow.AddHours(AccessTokenExpirationHours) + .ToString(CultureInfo.InvariantCulture), + RefreshExpirationDate = DateTime.UtcNow.AddDays(182).ToString(CultureInfo.InvariantCulture) + }, + Account = new UserInfo + { + Id = existingUser.Id, + Username = existingUser.Username, + Email = existingUser.Email, + Role = existingUser.Role.ToString() + } + }; + } + else + { + // Validate OTP using the new OTP service + if (!await otpService.ValidateOtpAsync(request.Email, request.Code, OtpPurpose.Login)) + throw new UnauthorizedException("Invalid or expired OTP"); + + var existingUser = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Email == request.Email && u.IsActive); + + if (existingUser is null) + { + var passwordHash = HashPassword("UserLoginViaOTP@123"); // Temporary password for OTP login + + var user = new User + { + Username = request.Email.Split('@')[0], + Email = request.Email, + PasswordHash = passwordHash, + Role = UserRole.User, + EmailVerified = true, // Email is verified via OTP + IsActive = true, + LastLogin = DateTime.UtcNow + }; + + user.AvatarUrl = $"https://api.dicebear.com/9.x/adventurer/png?seed={user.Username}"; + + await _unitOfWork.GetRepository().Insert(user); + await _unitOfWork.CommitAsync(); + + // Generate tokens + var token = jwtService.GenerateToken(user); + var refreshToken = jwtService.GenerateRefreshToken(user); + + // Save refresh token + await jwtTokenService.SaveTokenAsync(user, token, refreshToken, true); + await _unitOfWork.CommitAsync(); + + return new LoginResponse + { + Token = new TokenResponse + { + AccessToken = token, + RefreshToken = refreshToken, + ExpirationDate = DateTime.UtcNow.AddHours(AccessTokenExpirationHours) + .ToString(CultureInfo.InvariantCulture), + RefreshExpirationDate = DateTime.UtcNow.AddDays(182).ToString(CultureInfo.InvariantCulture) + }, + Account = new UserInfo + { + Id = user.Id, + Username = user.Username, + Email = user.Email, + Role = user.Role.ToString() + } + }; + } + else + { + // Update last login + existingUser.LastLogin = DateTime.UtcNow; + await _unitOfWork.GetRepository().Update(existingUser); + await _unitOfWork.CommitAsync(); + + // Generate tokens + var token = jwtService.GenerateToken(existingUser); + var refreshToken = jwtService.GenerateRefreshToken(existingUser); + + // Save refresh token + await jwtTokenService.SaveTokenAsync(existingUser, token, refreshToken, true); + await _unitOfWork.CommitAsync(); + + return new LoginResponse + { + Token = new TokenResponse + { + AccessToken = token, + RefreshToken = refreshToken, + ExpirationDate = DateTime.UtcNow.AddHours(AccessTokenExpirationHours) + .ToString(CultureInfo.InvariantCulture), + RefreshExpirationDate = DateTime.UtcNow.AddDays(182).ToString(CultureInfo.InvariantCulture) + }, + Account = new UserInfo + { + Id = existingUser.Id, + Username = existingUser.Username, + Email = existingUser.Email, + Role = existingUser.Role.ToString() + } + }; + } + } + + } + + public async Task ResendOtpAsync(string email) + { + // Check OTP quota using the new OTP service + if (!await otpService.CanRequestOtpAsync(email)) + throw new TooManyRequestsException(CommonExceptionMessage.OtpResendLimitExceeded); + + // Generate and save new OTP + var otp = await otpService.GenerateOtpAsync(email, OtpPurpose.Login); + + // Send OTP via email + await SendOtpEmailAsync(email, otp); + + var remainingAttempts = await otpService.GetRemainingAttemptsAsync(email); + + _logger.LogInformation("OTP resent to email: {Email}", email); + + return new ResendOtpResponse + { + Message = "OTP sent successfully", + Email = email.ToLower(), + RemainingAttempts = remainingAttempts, + ExpiresInMinutes = 5 + }; + } + + public async Task OnboardAuth0UserAsync(Auth0OnboardingRequest request) + { + // Cấu hình hằng số cho Email dự phòng (phải khớp với Auth0 Action) + const string FALLBACK_EMAIL_PREFIX = "no-reply-"; + + // Kiểm tra xem email có phải là email dự phòng hay không + var isFallbackEmail = request.Email.StartsWith(FALLBACK_EMAIL_PREFIX); + + // Verify internal secret (Giữ nguyên) + var configuredSecret = configuration["Auth0:InternalSyncSecret"]; + if (string.IsNullOrEmpty(configuredSecret) || request.InternalSecret != configuredSecret) + { + throw new UnauthorizedException("Invalid internal sync secret"); + } + + // 1. Check if user already exists by Auth0UserId (Liên kết Auth0-Internal) (Giữ nguyên) + var existingUser = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Auth0Id == request.Auth0UserId && u.IsActive); + + if (existingUser != null) + { + return new Auth0OnboardingResponse + { + InternalUserId = existingUser.Id, + Email = existingUser.Email, + Username = existingUser.Username, + IsNewUser = false + }; + } + + // 2. Check if user exists by email (Chỉ chạy nếu KHÔNG phải email dự phòng) + if (!isFallbackEmail) + { + var existingUserByEmail = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Email == request.Email && u.IsActive); + + if (existingUserByEmail != null) + { + // Update existing user with Auth0UserId (Liên kết tài khoản cũ) + existingUserByEmail.Auth0Id = request.Auth0UserId; + await _unitOfWork.GetRepository().Update(existingUserByEmail); + await _unitOfWork.CommitAsync(); + + return new Auth0OnboardingResponse + { + InternalUserId = existingUserByEmail.Id, + Email = existingUserByEmail.Email, + Username = existingUserByEmail.Username, + IsNewUser = false + }; + } + } + + // 3. Create new user + var newUser = new User + { + Id = Guid.NewGuid(), + Auth0Id = request.Auth0UserId, + Email = request.Email, + Username = request.Username, // Dùng thẳng Username từ request (không kiểm tra trùng lặp) + PasswordHash = HashPassword(Guid.NewGuid().ToString()), + Role = UserRole.User, + // Đánh dấu EmailVerified = true CHỈ khi email KHÔNG phải là email dự phòng + EmailVerified = !isFallbackEmail, + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().Insert(newUser); + await _unitOfWork.CommitAsync(); + + return new Auth0OnboardingResponse + { + InternalUserId = newUser.Id, + Email = newUser.Email, + Username = newUser.Username, + IsNewUser = true + }; + } + + #region Private Methods + + private bool VerifyPassword(string password, string hash) + { + return BCrypt.Net.BCrypt.Verify(password, hash); + } + + private string HashPassword(string password) + { + return BCrypt.Net.BCrypt.HashPassword(password); + } + + private async Task SendOtpEmailAsync(string email, string otp) + { + await emailService.SendOtpEmailAsync(email, otp); + _logger.LogInformation("OTP {Otp} would be sent to {Email}", otp, email); + } + + #endregion +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/B2FileAccessService.cs b/TaskGenix.BLL/Services/Implements/B2FileAccessService.cs new file mode 100644 index 0000000..6818741 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/B2FileAccessService.cs @@ -0,0 +1,141 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Threading.Tasks; +//using Amazon.Runtime.Internal.Util; +//using Amazon.S3; +//using Amazon.S3.Model; +//using TaskGenix.BLL.Services; +//using MapsterMapper; +//using Microsoft.AspNetCore.Http; +//using Microsoft.Extensions.Logging; +//using TaskGenix.BLL.Services.Interfaces; +//using TaskGenix.Core.Domains; +//using TaskGenix.DAL.Data; +//using TaskGenix.Repositories.Interfaces; + +//namespace TaskGenix.BLL.Services.Implements +//{ +// public class B2FileAccessService : BaseService, IFileAccessService +// { +// private readonly IAmazonS3 _s3; + +// public B2FileAccessService(IUnitOfWork unitOfWork, +// ILogger logger, +// IHttpContextAccessor httpContextAccessor, +// IMapper mapper, +// IAmazonS3 s3) +// : base(unitOfWork, logger, httpContextAccessor, mapper) +// { +// _s3 = s3; +// } + +// public async Task GeneratePresignedUrlAsync(Recording recording, TimeSpan expiration) +// { +// return await GeneratePresignedUrlAsync(recording.BucketName, recording.ObjectKey, expiration); +// } + +// public async Task GeneratePresignedUrlAsync(string bucketName, string objectKey, TimeSpan expiration) +// { +// try +// { +// var request = new GetPreSignedUrlRequest +// { +// BucketName = bucketName, +// Key = objectKey, +// Verb = HttpVerb.GET, +// Expires = DateTime.UtcNow.Add(expiration), +// ResponseHeaderOverrides = new ResponseHeaderOverrides +// { +// ContentDisposition = "attachment" // Force download instead of inline +// } +// }; + +// var presignedUrl = await Task.FromResult(_s3.GetPreSignedURL(request)); +// _logger.LogDebug("Generated presigned URL for {ObjectKey}, expires in {Expiration}", objectKey, expiration); + +// return presignedUrl; +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "Failed to generate presigned URL for {BucketName}/{ObjectKey}", bucketName, objectKey); +// throw; +// } +// } + +// public async Task DownloadFileAsync(Recording recording) +// { +// try +// { +// var request = new GetObjectRequest +// { +// BucketName = recording.BucketName, +// Key = recording.ObjectKey +// }; + +// using var response = await _s3.GetObjectAsync(request); +// using var memoryStream = new MemoryStream(); +// await response.ResponseStream.CopyToAsync(memoryStream); + +// _logger.LogDebug("Downloaded file {ObjectKey}, size: {Size} bytes", recording.ObjectKey, memoryStream.Length); +// return memoryStream.ToArray(); +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "Failed to download file {BucketName}/{ObjectKey}", recording.BucketName, recording.ObjectKey); +// throw; +// } +// } + +// public async Task GetFileStreamAsync(Recording recording) +// { +// try +// { +// var request = new GetObjectRequest +// { +// BucketName = recording.BucketName, +// Key = recording.ObjectKey +// }; + +// var response = await _s3.GetObjectAsync(request); + +// // Return the stream directly for streaming scenarios +// // Note: Caller is responsible for disposing the stream +// return response.ResponseStream; +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "Failed to get file stream {BucketName}/{ObjectKey}", recording.BucketName, recording.ObjectKey); +// throw; +// } +// } + +// public async Task GetTemporaryFilePathAsync(Recording recording) +// { +// try +// { +// var tempFileName = Path.GetTempFileName(); +// var extension = Path.GetExtension(recording.ObjectKey); +// var tempFilePath = Path.ChangeExtension(tempFileName, extension); + +// var request = new GetObjectRequest +// { +// BucketName = recording.BucketName, +// Key = recording.ObjectKey +// }; + +// using var response = await _s3.GetObjectAsync(request); +// using var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write); +// await response.ResponseStream.CopyToAsync(fileStream); + +// _logger.LogDebug("Downloaded file to temp path: {TempPath}", tempFilePath); +// return tempFilePath; +// } +// catch (Exception ex) +// { +// _logger.LogError(ex, "Failed to download file to temp path {BucketName}/{ObjectKey}", recording.BucketName, recording.ObjectKey); +// throw; +// } +// } +// } +//} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/BaseService.cs b/TaskGenix.BLL/Services/Implements/BaseService.cs index bbcd221..7904c12 100644 --- a/TaskGenix.BLL/Services/Implements/BaseService.cs +++ b/TaskGenix.BLL/Services/Implements/BaseService.cs @@ -1,49 +1,52 @@ using MapsterMapper; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; +using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using TaskGenix.Core.Domains; using TaskGenix.Core.Exceptions; using TaskGenix.DAL.Data; using TaskGenix.Repositories.Interfaces; -namespace ClaimRequest.BLL.Services +namespace TaskGenix.BLL.Services; + +public abstract class BaseService where T : class { - public abstract class BaseService where T : class + protected IUnitOfWork _unitOfWork; + protected ILogger _logger; + protected IHttpContextAccessor _httpContextAccessor; + protected IMapper _mapper; + + public BaseService(IUnitOfWork unitOfWork, ILogger logger, + IHttpContextAccessor httpContextAccessor, IMapper mapper) { - protected IUnitOfWork _unitOfWork; - protected ILogger _logger; - protected IHttpContextAccessor _httpContextAccessor; - protected IMapper _mapper; + _unitOfWork = unitOfWork; + _logger = logger; + _httpContextAccessor = httpContextAccessor; + _mapper = mapper; + } - public BaseService(IUnitOfWork unitOfWork, ILogger logger, IHttpContextAccessor httpContextAccessor, IMapper mapper) - { - _unitOfWork = unitOfWork; - _logger = logger; - _httpContextAccessor = httpContextAccessor; - _mapper = mapper; - } + protected Guid GetCurrentUserId() + { + var userIdClaim = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) + throw new UnauthorizedException("User ID not found in token"); + return userId; + } - protected Guid GetCurrentUserId() - { - var userIdClaim = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out Guid userId)) - { - throw new UnauthorizedException("User ID not found in token"); - } - return userId; - } + protected string GetCurrentUserEmail() + { + // Try multiple claim types to find the email + var emailClaim = _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.Email)?.Value ?? + _httpContextAccessor.HttpContext?.User?.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value ?? + _httpContextAccessor.HttpContext?.User?.FindFirst("email")?.Value; - protected string GetCurrentUserEmail() - { - return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.Email)?.Value - ?? throw new UnauthorizedException("User email not found in token"); - } + return emailClaim ?? throw new UnauthorizedException("User email not found in token"); + } - protected string GetCurrentUserRole() - { - return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.Role)?.Value - ?? throw new UnauthorizedException("User role not found in token"); - } + protected string GetCurrentUserRole() + { + return _httpContextAccessor.HttpContext?.User?.FindFirst(ClaimTypes.Role)?.Value + ?? throw new UnauthorizedException("User role not found in token"); } -} +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/EmailService.cs b/TaskGenix.BLL/Services/Implements/EmailService.cs new file mode 100644 index 0000000..d45fbb9 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/EmailService.cs @@ -0,0 +1,122 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Net; +using System.Net.Mail; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Constants; +using TaskGenix.Core.Settings; + +namespace TaskGenix.BLL.Services.Implements; + +public class EmailService(IOptions emailSettings, ILogger logger) + : IEmailService +{ + private readonly EmailSettings _emailSettings = emailSettings.Value; + + public async Task SendOtpEmailAsync(string toEmail, string otpCode) + { + var subject = "🔐 TaskGenix - Your OTP Code"; + + var template = TemplateEmail.OtpTemplate; + var values = new Dictionary + { + ["OtpCode"] = otpCode, + ["SupportEmail"] = _emailSettings.FromEmail, + ["Year"] = DateTime.UtcNow.Year.ToString() + }; + + var htmlBody = FillTemplate(template, values); + var plainText = $"Your TaskGenix OTP code is {otpCode}. It will expire in 5 minutes."; + + await SendEmailAsync(toEmail, subject, htmlBody, plainText); + } + + public async Task SendLoginSuccessEmailAsync(string toEmail) + { + var subject = "✅ TaskGenix - Login Successful"; + var template = TemplateEmail.LoginSuccessTemplate; + var values = new Dictionary + { + ["UserEmail"] = toEmail, + ["SupportEmail"] = _emailSettings.FromEmail, + ["Year"] = DateTime.UtcNow.Year.ToString() + }; + + var htmlBody = FillTemplate(template, values); + var plainText = $"You have successfully logged into your TaskGenix account. If this wasn't you, please reset your password immediately."; + + await SendEmailAsync(toEmail, subject, htmlBody, plainText); + } + + public async Task SendNotificationEmailAsync(string toEmail, string subject, string message) + { + var template = TemplateEmail.NotificationTemplate; + var values = new Dictionary + { + ["Message"] = message, + ["SupportEmail"] = _emailSettings.FromEmail, + ["Year"] = DateTime.UtcNow.Year.ToString() + }; + + var htmlBody = FillTemplate(template, values); + var plainText = message; + + await SendEmailAsync(toEmail, subject, htmlBody, plainText); + } + + private async Task SendEmailAsync(string toEmail, string subject, string htmlBody, string? plainText = null) + { + try + { + if (string.IsNullOrEmpty(_emailSettings.SmtpServer) || + string.IsNullOrEmpty(_emailSettings.Username) || + string.IsNullOrEmpty(_emailSettings.Password)) + { + logger.LogWarning("Email settings not configured. Email will not be sent."); + return; + } + + using var client = new SmtpClient(_emailSettings.SmtpServer, _emailSettings.SmtpPort) + { + EnableSsl = true, + Credentials = new NetworkCredential(_emailSettings.Username, _emailSettings.Password) + }; + + var mailMessage = new MailMessage + { + From = new MailAddress(_emailSettings.FromEmail, _emailSettings.FromName), + Subject = subject, + IsBodyHtml = true + }; + + mailMessage.To.Add(toEmail); + mailMessage.Body = htmlBody; + + if (!string.IsNullOrEmpty(plainText)) + { + var htmlView = AlternateView.CreateAlternateViewFromString(htmlBody, null, "text/html"); + var textView = AlternateView.CreateAlternateViewFromString(plainText, null, "text/plain"); + mailMessage.AlternateViews.Add(textView); + mailMessage.AlternateViews.Add(htmlView); + } + + await client.SendMailAsync(mailMessage); + logger.LogInformation("Email sent successfully to {Email}", toEmail); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to send email to {Email}", toEmail); + throw; + } + } + + private string FillTemplate(string template, IDictionary values) + { + foreach (var kv in values) + { + template = template.Replace("{{" + kv.Key + "}}", kv.Value ?? string.Empty, StringComparison.OrdinalIgnoreCase); + } + + return template; + } +} diff --git a/TaskGenix.BLL/Services/Implements/FileAccessService.cs b/TaskGenix.BLL/Services/Implements/FileAccessService.cs new file mode 100644 index 0000000..2db18dd --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/FileAccessService.cs @@ -0,0 +1,87 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Amazon.S3; +using Amazon.S3.Model; +using Microsoft.Extensions.Configuration; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; + +namespace TaskGenix.BLL.Services.Implements +{ + public class FileAccessService : IFileAccessService + { + private readonly IAmazonS3 _s3; + private readonly IConfiguration _config; + + public FileAccessService(IAmazonS3 s3, IConfiguration config) + { + _s3 = s3; + _config = config; + } + + public async Task GeneratePresignedUrlAsync(Recording recording, TimeSpan expiration) + { + var bucket = _config["CloudflareR2:BucketName"] ?? throw new InvalidOperationException("Cloudflare R2 bucket not configured"); + return await GeneratePresignedUrlAsync(bucket, recording.ObjectKey, expiration); + } + + public async Task GeneratePresignedUrlAsync(string bucketName, string objectKey, TimeSpan expiration) + { + var request = new GetPreSignedUrlRequest + { + BucketName = bucketName, + Key = objectKey, + Expires = DateTime.UtcNow.Add(expiration) + }; + return await _s3.GetPreSignedURLAsync(request); + } + + public async Task DownloadFileAsync(Recording recording) + { + var bucket = _config["CloudflareR2:BucketName"] ?? throw new InvalidOperationException("Cloudflare R2 bucket not configured"); + var request = new GetObjectRequest + { + BucketName = bucket, + Key = recording.ObjectKey + }; + using var response = await _s3.GetObjectAsync(request); + using var ms = new MemoryStream(); + await response.ResponseStream.CopyToAsync(ms); + return ms.ToArray(); + } + + public async Task GetFileStreamAsync(Recording recording) + { + var bucket = _config["CloudflareR2:BucketName"] ?? throw new InvalidOperationException("Cloudflare R2 bucket not configured"); + var request = new GetObjectRequest + { + BucketName = bucket, + Key = recording.ObjectKey + }; + var response = await _s3.GetObjectAsync(request); + return response.ResponseStream; + } + + public async Task GetTemporaryFilePathAsync(Recording recording) + { + var data = await DownloadFileAsync(recording); + var tempPath = Path.GetTempFileName(); + await File.WriteAllBytesAsync(tempPath, data); + return tempPath; + } + + public async Task UploadFileAsync(string bucketName, string objectKey, Stream fileStream, string contentType) + { + var request = new PutObjectRequest + { + BucketName = bucketName, + Key = objectKey, + InputStream = fileStream, + ContentType = contentType, + AutoCloseStream = false + }; + await _s3.PutObjectAsync(request); + } + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/GeminiChatCompletionService.cs b/TaskGenix.BLL/Services/Implements/GeminiChatCompletionService.cs new file mode 100644 index 0000000..a11ebbb --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/GeminiChatCompletionService.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Services; + +namespace TaskGenix.BLL.Services.Implements +{ + public class GeminiChatCompletionService : IChatCompletionService + { + private readonly HttpClient _httpClient; + private readonly string _apiKey; + private readonly string _modelId; + private readonly ILogger _logger; + + public IReadOnlyDictionary Attributes => new Dictionary(); + + public GeminiChatCompletionService( + string modelId, + string apiKey, + HttpClient? httpClient = null, + ILogger? logger = null) + { + _modelId = modelId ?? throw new ArgumentNullException(nameof(modelId)); + _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); + _httpClient = httpClient ?? new HttpClient(); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task> GetChatMessageContentsAsync( + ChatHistory chatHistory, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + { + try + { + _logger.LogInformation("Starting Gemini API call for model: {ModelId}", _modelId); + + var request = CreateGeminiRequest(chatHistory); + + var jsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = false + }; + + var jsonContent = JsonSerializer.Serialize(request, jsonOptions); + + _logger.LogDebug("Request payload: {Payload}", jsonContent); var httpRequest = new HttpRequestMessage(HttpMethod.Post, + $"https://generativelanguage.googleapis.com/v1beta/models/{_modelId}:generateContent?key={_apiKey}") + { + Content = new StringContent(jsonContent, Encoding.UTF8, "application/json") + }; + + var response = await _httpClient.SendAsync(httpRequest, cancellationToken); + + _logger.LogInformation("Received response with status: {StatusCode}", response.StatusCode); + + if (!response.IsSuccessStatusCode) + { + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + _logger.LogError("Gemini API error: {StatusCode} - {Error}", response.StatusCode, errorContent); + throw new HttpRequestException($"Gemini API returned {response.StatusCode}: {errorContent}"); + } + + var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); + _logger.LogDebug("Response content: {Content}", responseContent); + + var responseJsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + + var geminiResponse = JsonSerializer.Deserialize(responseContent, responseJsonOptions); + + if (geminiResponse?.Candidates?.Count > 0) + { + var candidate = geminiResponse.Candidates[0]; + if (candidate.Content?.Parts?.Count > 0) + { + var part = candidate.Content.Parts[0]; + var text = part.Text ?? string.Empty; + return new List + { + new ChatMessageContent(AuthorRole.Assistant, text) + }; + } + } + _logger.LogWarning("No valid response received from Gemini API"); + return new List + { + new ChatMessageContent(AuthorRole.Assistant, "No response generated") + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error calling Gemini API"); + throw; + } + } + + public IAsyncEnumerable GetStreamingChatMessageContentsAsync( + ChatHistory chatHistory, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException("Streaming is not implemented for this Gemini service"); + } + + private static GeminiRequest CreateGeminiRequest(ChatHistory chatHistory) + { + var contents = new List(); + + foreach (var message in chatHistory) + { + var role = message.Role == AuthorRole.User ? "user" : "model"; + contents.Add(new GeminiContent + { + Role = role, + Parts = new List + { + new GeminiPart { Text = message.Content ?? string.Empty } + } + }); + } + + return new GeminiRequest + { + Contents = contents + }; + } + } + + // Gemini API models + public class GeminiRequest + { + public List Contents { get; set; } = new(); + } + + public class GeminiContent + { + public string Role { get; set; } = string.Empty; + public List Parts { get; set; } = new(); + } + + public class GeminiPart + { + public string Text { get; set; } = string.Empty; + } + + public class GeminiResponse + { + public List? Candidates { get; set; } + } + + public class GeminiCandidate + { + public GeminiContent? Content { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/JwtService.cs b/TaskGenix.BLL/Services/Implements/JwtService.cs new file mode 100644 index 0000000..f354516 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/JwtService.cs @@ -0,0 +1,108 @@ +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Configuration; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; + +namespace TaskGenix.BLL.Services.Implements; + +public class JwtService : IJwtService +{ + private readonly IConfiguration _configuration; + + public JwtService(IConfiguration configuration) + { + _configuration = configuration; + } + + + public string GenerateToken(User user) + { + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"])); + var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), + new Claim(JwtRegisteredClaimNames.UniqueName, user.Email), + new Claim(JwtRegisteredClaimNames.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var tokenOptions = new JwtSecurityToken( + _configuration["Jwt:Issuer"], + _configuration["Jwt:Audience"], + claims, + expires: DateTime.Now.AddMinutes(Convert.ToDouble(_configuration["Jwt:ExpirationInMinutes"])), + signingCredentials: signinCredentials + ); + + + return new JwtSecurityTokenHandler().WriteToken(tokenOptions); + } + + public string GenerateRefreshToken(User user) + { + var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:RefreshSecretKey"])); + var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256); + + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()), + new Claim(JwtRegisteredClaimNames.UniqueName, user.Email), + new Claim(JwtRegisteredClaimNames.Email, user.Email), + new Claim(ClaimTypes.Role, user.Role.ToString()), + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + var tokenOptions = new JwtSecurityToken( + _configuration["Jwt:Issuer"], + _configuration["Jwt:Audience"], + claims, + expires: DateTime.Now.AddDays(Convert.ToDouble(_configuration["Jwt:RefreshExpirationInDays"])), + signingCredentials: signinCredentials + ); + + return new JwtSecurityTokenHandler().WriteToken(tokenOptions); + } + + public ClaimsPrincipal DecodeToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + var validationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = _configuration["Jwt:Issuer"], + ValidAudience = _configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"])) + }; + + try + { + // Validate the token and get the principal + var principal = tokenHandler.ValidateToken(token, validationParameters, out var validatedToken); + return principal; + } + catch (Exception) + { + // Token validation failed + return null; + } + } + + public JwtSecurityToken ReadTokenWithoutValidation(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + + if (tokenHandler.CanReadToken(token)) return tokenHandler.ReadJwtToken(token); + + return null; + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/JwtTokenService.cs b/TaskGenix.BLL/Services/Implements/JwtTokenService.cs new file mode 100644 index 0000000..3d4cb44 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/JwtTokenService.cs @@ -0,0 +1,149 @@ +using TaskGenix.BLL.Services; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements; + +public class JwtTokenService( + IUnitOfWork unitOfWork, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper +) : BaseService(unitOfWork, logger, httpContextAccessor, mapper), IJwtTokenService +{ + private const int AccessTokenExpirationHours = 1; + private const int RefreshTokenExpirationDays = 30; + + public async Task SaveTokenAsync(User user, string accessToken, string refreshToken, + bool isMobile = false) + { + var jwtToken = new JwtToken + { + UserId = user.Id, + AccessToken = accessToken, + RefreshToken = refreshToken, + ExpirationDate = DateTime.UtcNow.AddHours(AccessTokenExpirationHours), + RefreshTokenExpirationDate = DateTime.UtcNow.AddDays(RefreshTokenExpirationDays), + IsMobile = isMobile, + DeviceInfo = GetDeviceInfo(), + IpAddress = GetClientIpAddress() + }; + + await _unitOfWork.GetRepository().Insert(jwtToken); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("JWT token saved for user: {UserId}, IsMobile: {IsMobile}", user.Id, isMobile); + + return jwtToken; + } + + public async Task ValidateRefreshTokenAsync(string refreshToken) + { + var jwtToken = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: t => t.RefreshToken == refreshToken && + !t.IsRevoked && + !t.IsExpired && + t.RefreshTokenExpirationDate > DateTime.UtcNow, + include: q => q.Include(t => t.User) + ); + + if (jwtToken == null) + { + _logger.LogWarning("Invalid or expired refresh token: {RefreshToken}", refreshToken); + throw new UnauthorizedException("Invalid or expired refresh token"); + } + + if (!jwtToken.User.IsActive) + { + _logger.LogWarning("User is not active for refresh token: {UserId}", jwtToken.UserId); + throw new UnauthorizedException("User account is not active"); + } + + return jwtToken.User; + } + + public async Task InvalidateTokenAsync(string refreshToken) + { + var jwtToken = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: t => t.RefreshToken == refreshToken); + + if (jwtToken != null) + { + jwtToken.IsRevoked = true; + jwtToken.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(jwtToken); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("JWT token invalidated: {TokenId}", jwtToken.Id); + } + } + + public async Task InvalidateAllUserTokensAsync(Guid userId) + { + var userTokens = await _unitOfWork.GetRepository() + .GetListAsync(predicate: t => t.UserId == userId && !t.IsRevoked); + + foreach (var token in userTokens) + { + token.IsRevoked = true; + token.UpdatedAt = DateTime.UtcNow; + await _unitOfWork.GetRepository().Update(token); + } + + await _unitOfWork.CommitAsync(); + _logger.LogInformation("All JWT tokens invalidated for user: {UserId}, Count: {Count}", userId, + userTokens.Count()); + } + + public async Task GetTokenByRefreshTokenAsync(string refreshToken) + { + return await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: t => t.RefreshToken == refreshToken, + include: q => q.Include(t => t.User) + ); + } + + public async Task CleanupExpiredTokensAsync() + { + var expiredTokens = await _unitOfWork.GetRepository() + .GetListAsync(predicate: t => + t.RefreshTokenExpirationDate < DateTime.UtcNow || + t.IsRevoked); + + foreach (var token in expiredTokens) await _unitOfWork.GetRepository().Delete(token); + + await _unitOfWork.CommitAsync(); + _logger.LogInformation("Cleaned up {Count} expired JWT tokens", expiredTokens.Count()); + } + + private string? GetDeviceInfo() + { + var context = _httpContextAccessor.HttpContext; + if (context == null) return null; + + var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault(); + return userAgent?.Length > 100 ? userAgent.Substring(0, 100) : userAgent; + } + + private string? GetClientIpAddress() + { + var context = _httpContextAccessor.HttpContext; + if (context == null) return null; + + var ipAddress = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (string.IsNullOrEmpty(ipAddress)) ipAddress = context.Request.Headers["X-Real-IP"].FirstOrDefault(); + if (string.IsNullOrEmpty(ipAddress)) ipAddress = context.Connection.RemoteIpAddress?.ToString(); + + return ipAddress; + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/NotificationService.cs b/TaskGenix.BLL/Services/Implements/NotificationService.cs new file mode 100644 index 0000000..61067c7 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/NotificationService.cs @@ -0,0 +1,230 @@ +using TaskGenix.BLL.Services; +using FirebaseAdmin.Messaging; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Notification; +using TaskGenix.Core.Responses.Notification; +using Mapster; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements +{ + public class NotificationService : BaseService, INotificationService + { + + private readonly IUserDeviceTokenService _userDeviceTokenService; + + public NotificationService( + IUnitOfWork unitOfWork, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper, + IUserDeviceTokenService userDeviceTokenService) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + _userDeviceTokenService = userDeviceTokenService; + } + + public async Task> GetAllNotificationsAsync(int page = 1, int size = 20) + { + try + { + var pagedNotifications = await _unitOfWork.GetRepository() + .GetPagingListAsync( + selector: n => n.Adapt(), + orderBy: query => query.OrderByDescending(n => n.CreatedAt), + page: page, + size: size); + + return pagedNotifications; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get all notifications"); + throw; + } + } + + public async Task> GetNotificationsByEmailAsync(string email) + { + try + { + var notifications = await _unitOfWork.GetRepository() + .GetListAsync( + selector: n => n.Adapt(), + predicate: n => n.Email == email, + orderBy: query => query.OrderByDescending(n => n.CreatedAt)); + + return notifications; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get notifications for user: {Email}", email); + throw; + } + } + + public async Task CreateNotificationAsync(Guid userId, CreateNotificationRequest request) + { + + var taskId = await _unitOfWork + .GetRepository() + .FirstOrDefaultAsync(predicate: t => t.Id == request.TaskGenId) + ?? throw new NotFoundException($"TaskGen with ID {request.TaskGenId} not found"); + + try + { + var notification = new Core.Domains.Notification + { + Title = request.Title, + Message = request.Message, + Icon = request.Icon, + Color = request.Color, + Type = request.Type, + TaskGenId = taskId.Id, + ScheduledTime = request.ScheduledTime, + // SentAt = DateTime.Now, + IsSent = false, + IsRead = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().Insert(notification); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Notification created for user: {UserId}", userId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create notification for user: {UserId}", userId); + throw; + } + } + + public async Task MarkAsReadAsync(Guid notificationId) + { + try + { + var notification = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: n => n.Id == notificationId); + + if (notification == null) + { + throw new KeyNotFoundException($"Notification with ID {notificationId} not found"); + } + + notification.IsRead = true; + notification.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(notification); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Notification marked as read: {NotificationId}", notificationId); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to mark notification as read: {NotificationId}", notificationId); + throw; + } + } + + public async Task SendPushNotificationAsync(string token, string title, string body) + { + try + { + // TODO: Implement FCM push notification sending + // This is a placeholder implementation + _logger.LogInformation("Push notification would be sent to token: {Token}, Title: {Title}, Body: {Body}", + token, title, body); + + await Task.CompletedTask; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send push notification"); + throw; + } + } + + public async Task SendNotificationToUserAsync(string email, string title, string body, NotificationType type) + { + try + { + // Get all FCM tokens for the user + var fcmTokens = await _userDeviceTokenService.GetFcmTokensByUserEmailAsync(email); + + // Save notification to database + await SaveNotificationForUserAsync(email, title, body, type); + + // Send push notification to all user devices + foreach (var token in fcmTokens) + { + await SendPushNotificationAsync(token, title, body); + } + + _logger.LogInformation("Notification sent to all devices for user: {Email}", email); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send notification to user: {Email}", email); + throw; + } + } + + public async Task SaveNotificationForUserAsync(String email, string title, string description, NotificationType type) + { + try + { + var notification = new Core.Domains.Notification + { + Title = title, + Message = description, + Type = type, + Email = email, + IsSent = true, + IsRead = false, + SentAt = DateTime.UtcNow, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().Insert(notification); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Notification saved for user: {Email}", email); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to save notification for user: {Email}", email); + throw; + } + } + + public async Task SendMessageAsync( + string deviceToken, + string title, + string body) + { + var message = new Message() + { + Token = deviceToken, + Notification = new FirebaseAdmin.Messaging.Notification + { + Title = title, + Body = body + } + }; + + string response = await FirebaseMessaging.DefaultInstance.SendAsync(message); + return response; // messageId + } + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/OtpQuotaService.cs b/TaskGenix.BLL/Services/Implements/OtpQuotaService.cs new file mode 100644 index 0000000..48a4069 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/OtpQuotaService.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; + +namespace TaskGenix.BLL.Services.Implements; + +public class OtpQuotaService(IMemoryCache cache, ILogger logger) : IOtpQuotaService +{ + private readonly int _maxRequestsPerHour = 5; + private readonly TimeSpan _quotaWindow = TimeSpan.FromHours(1); + + public async Task CanRequestOtpAsync(string email) + { + var key = GetQuotaKey(email); + var requestCount = cache.Get(key) ?? 0; + + var canRequest = requestCount < _maxRequestsPerHour; + + logger.LogInformation("OTP quota check for {Email}: {RequestCount}/{MaxRequests} - Can request: {CanRequest}", + email, requestCount, _maxRequestsPerHour, canRequest); + + return await Task.FromResult(canRequest); + } + + public async Task IncrementOtpRequestAsync(string email) + { + var key = GetQuotaKey(email); + var currentCount = cache.Get(key) ?? 0; + var newCount = currentCount + 1; + + cache.Set(key, newCount, _quotaWindow); + + logger.LogInformation("OTP request incremented for {Email}: {NewCount}/{MaxRequests}", + email, newCount, _maxRequestsPerHour); + + await Task.CompletedTask; + } + + public async Task GetRemainingQuotaAsync(string email) + { + var key = GetQuotaKey(email); + var requestCount = cache.Get(key) ?? 0; + var remaining = Math.Max(0, _maxRequestsPerHour - requestCount); + + return await Task.FromResult(remaining); + } + + public async Task ResetQuotaAsync(string email) + { + var key = GetQuotaKey(email); + cache.Remove(key); + + logger.LogInformation("OTP quota reset for {Email}", email); + + await Task.CompletedTask; + } + + private string GetQuotaKey(string email) + { + return $"otp_quota_{email.ToLower()}"; + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/OtpService.cs b/TaskGenix.BLL/Services/Implements/OtpService.cs new file mode 100644 index 0000000..21aaa0a --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/OtpService.cs @@ -0,0 +1,171 @@ +using TaskGenix.BLL.Services; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements; + +public class OtpService( + IUnitOfWork unitOfWork, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper +) : BaseService(unitOfWork, logger, httpContextAccessor, mapper), IOtpService +{ + private const int OtpExpirationMinutes = 5; + private const int MaxOtpAttemptsPerHour = 5; + private const int MaxValidationAttempts = 3; + + public async Task GenerateOtpAsync(string email, OtpPurpose purpose) + { + // Check if user can request OTP + if (!await CanRequestOtpAsync(email)) + throw new TooManyRequestsException("Too many OTP requests. Please try again later."); + + // Invalidate any existing OTPs for this email and purpose + await InvalidateOtpAsync(email, purpose); + + // Generate new OTP + var otp = GenerateRandomOtp(); + var expirationDate = DateTime.UtcNow.AddMinutes(OtpExpirationMinutes); + + var otpCode = new OtpCode + { + Email = email.ToLowerInvariant(), + Code = otp, + ExpirationDate = expirationDate, + Purpose = purpose, + IpAddress = GetClientIpAddress() + }; + + await _unitOfWork.GetRepository().Insert(otpCode); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("OTP generated for email: {Email}, Purpose: {Purpose}", email, purpose); + + return otp; + } + + public async Task ValidateOtpAsync(string email, string otp, OtpPurpose purpose) + { + var otpRecord = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: o => + o.Email == email.ToLowerInvariant() && + o.Purpose == purpose && + !o.IsUsed && + o.ExpirationDate > DateTime.UtcNow); + + if (otpRecord == null) + { + _logger.LogWarning("OTP validation failed - no valid OTP found for email: {Email}", email); + return false; + } + + // Increment attempt count + otpRecord.AttemptCount++; + await _unitOfWork.GetRepository().Update(otpRecord); + + // Check if too many attempts + if (otpRecord.AttemptCount > MaxValidationAttempts) + { + otpRecord.IsUsed = true; + await _unitOfWork.GetRepository().Update(otpRecord); + await _unitOfWork.CommitAsync(); + + _logger.LogWarning("OTP validation failed - too many attempts for email: {Email}", email); + throw new UnauthorizedException("Too many failed attempts. Please request a new OTP."); + } + + // Validate OTP + if (otpRecord.Code == otp) + { + otpRecord.IsUsed = true; + await _unitOfWork.GetRepository().Update(otpRecord); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("OTP validated successfully for email: {Email}", email); + return true; + } + + await _unitOfWork.CommitAsync(); + _logger.LogWarning("OTP validation failed - invalid code for email: {Email}", email); + return false; + } + + public async Task InvalidateOtpAsync(string email, OtpPurpose purpose) + { + var otpCodes = await _unitOfWork.GetRepository() + .GetListAsync(predicate: o => + o.Email == email.ToLowerInvariant() && + o.Purpose == purpose && + !o.IsUsed); + + foreach (var otpCode in otpCodes) + { + otpCode.IsUsed = true; + await _unitOfWork.GetRepository().Update(otpCode); + } + + await _unitOfWork.CommitAsync(); + _logger.LogInformation("OTP invalidated for email: {Email}, Purpose: {Purpose}", email, purpose); + } + + public async Task CanRequestOtpAsync(string email) + { + var oneHourAgo = DateTime.UtcNow.AddHours(-1); + + var recentOtpCount = await _unitOfWork.GetRepository() + .CountAsync(o => + o.Email == email.ToLowerInvariant() && + o.CreatedAt > oneHourAgo); + + return recentOtpCount < MaxOtpAttemptsPerHour; + } + + public async Task GetRemainingAttemptsAsync(string email) + { + var oneHourAgo = DateTime.UtcNow.AddHours(-1); + + var recentOtpCount = await _unitOfWork.GetRepository() + .CountAsync(o => + o.Email == email.ToLowerInvariant() && + o.CreatedAt > oneHourAgo); + + return Math.Max(0, MaxOtpAttemptsPerHour - recentOtpCount); + } + + public async Task CleanupExpiredOtpsAsync() + { + var expiredOtps = await _unitOfWork.GetRepository() + .GetListAsync(predicate: o => o.ExpirationDate < DateTime.UtcNow || o.IsUsed); + + foreach (var expiredOtp in expiredOtps) await _unitOfWork.GetRepository().Delete(expiredOtp); + + await _unitOfWork.CommitAsync(); + _logger.LogInformation("Cleaned up {Count} expired OTPs", expiredOtps.Count()); + } + + private string GenerateRandomOtp() + { + var random = new Random(); + return random.Next(100000, 999999).ToString(); + } + + private string? GetClientIpAddress() + { + var context = _httpContextAccessor.HttpContext; + if (context == null) return null; + + var ipAddress = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); + if (string.IsNullOrEmpty(ipAddress)) ipAddress = context.Request.Headers["X-Real-IP"].FirstOrDefault(); + if (string.IsNullOrEmpty(ipAddress)) ipAddress = context.Connection.RemoteIpAddress?.ToString(); + + return ipAddress; + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/PaymentService.cs b/TaskGenix.BLL/Services/Implements/PaymentService.cs new file mode 100644 index 0000000..f666a9b --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/PaymentService.cs @@ -0,0 +1,478 @@ +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Net.payOS; +using Net.payOS.Types; +using System.Text.Json; +using Mapster; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Auth; +using TaskGenix.Core.Requests.Payment; +using TaskGenix.Core.Responses.PaymentResponse; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements +{ + public class PaymentService : BaseService, IPaymentService + { + private readonly PayOSConfig _payOSConfig; + private readonly PayOS _payOS; + private readonly string _baseUrl; + + public PaymentService(IUnitOfWork unitOfWork, IOptions payOSConfig, ILogger logger, IHttpContextAccessor httpContextAccessor, IMapper mapper, IConfiguration configuration) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + _payOSConfig = payOSConfig.Value; + _payOS = new PayOS(_payOSConfig.ClientId, _payOSConfig.ApiKey, _payOSConfig.ChecksumKey); + _baseUrl = configuration["BaseURL"] ?? "http://localhost"; + } + + public async Task> GetAllPaymentTransactionsAsync() + { + _logger.LogInformation("Getting all payment transactions"); + + return await _unitOfWork.GetRepository() + .GetListAsync(); + } + + public async Task CreatePaymentLinkAsync(PaymentRequest request) + { + try + { + var orderCode = int.Parse(DateTimeOffset.Now.ToString("ffffff")); + var listItems = new List + { + new ItemData("Gói dịch vụ TaskGenix", 1, (int)(request.Amount * 100)) + }; + var paymentLinkRequest = new PaymentData( + orderCode: orderCode, + amount: (int)(request.Amount * 100), + description: $"Thanh toán gói dịch vụ", + items: listItems, + returnUrl: $"{_baseUrl}/payment/success", + cancelUrl: $"{_baseUrl}/payment/cancel" + ); + + var response = await _payOS.createPaymentLink(paymentLinkRequest); + + if (response != null && !string.IsNullOrEmpty(response.checkoutUrl)) + { + // Lưu transaction vào database + var paymentTransaction = new PaymentTransaction + { + Amount = request.Amount, + Currency = request.Currency, + PaymentMethod = request.PaymentMethod, + PaymentGateway = "PayOS", + UserId = request.UserId, + UserPlanId = request.UserPlanId, + Status = PaymentStatus.Pending, + ExternalTransactionId = orderCode.ToString(), + TransactionDate = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().Insert(paymentTransaction); + await _unitOfWork.CommitAsync(); + + return new PaymentResponse + { + Success = true, + Message = "Payment link created successfully.", + TransactionId = orderCode.ToString(), + CheckoutUrl = response.checkoutUrl + }; + } + + return new PaymentResponse + { + Success = false, + Message = "Failed to create payment link." + }; + } + catch (Exception ex) + { + return new PaymentResponse + { + Success = false, + Message = $"Error creating payment link: {ex.Message}" + }; + } + } + + public async Task HandleWebhookAsync(object webhookData) + { + try + { + var webhookJson = JsonSerializer.Serialize(webhookData); + var webhook = JsonSerializer.Deserialize(webhookJson); + + if (webhook == null) + { + return new PaymentResponse + { + Success = false, + Message = "Invalid webhook data" + }; + } + + // Tìm transaction theo order code + var transaction = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: pt => pt.ExternalTransactionId == webhook.data.orderCode.ToString()); + + if (transaction == null) + { + return new PaymentResponse + { + Success = false, + Message = "Transaction not found" + }; + } + + // Cập nhật trạng thái transaction + if (webhook.data.status == "PAID") + { + transaction.Status = PaymentStatus.Completed; + } + else if (webhook.data.status == "CANCELLED") + { + transaction.Status = PaymentStatus.Cancelled; + } + else if (webhook.data.status == "FAILED") + { + transaction.Status = PaymentStatus.Failed; + } + + await _unitOfWork.GetRepository().Update(transaction); + await _unitOfWork.CommitAsync(); + + return new PaymentResponse + { + Success = true, + Message = "Webhook processed successfully" + }; + } + catch (Exception ex) + { + return new PaymentResponse + { + Success = false, + Message = $"Error processing webhook: {ex.Message}" + }; + } + } + public async Task HandlePaymentSuccessAsync(string orderCode) + { + try + { + _logger.LogInformation("Handling payment success for orderCode: {OrderCode}", orderCode); + + var paymentInfo = await _payOS.getPaymentLinkInformation(int.Parse(orderCode)); + + if (paymentInfo == null) + { + return new PaymentResponse + { + Success = false, + Message = "Payment not found" + }; + } + + // Find the transaction + var transaction = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: pt => pt.ExternalTransactionId == orderCode); + + if (transaction == null) + { + return new PaymentResponse + { + Success = false, + Message = "Transaction not found" + }; + } + + // Update transaction status based on payment info status + if (paymentInfo.status == "PAID" && transaction.Status != PaymentStatus.Completed) + { + transaction.Status = PaymentStatus.Completed; + await _unitOfWork.GetRepository().Update(transaction); + await _unitOfWork.CommitAsync(); + } + else if (paymentInfo.status == "CANCELLED" && transaction.Status != PaymentStatus.Cancelled) + { + transaction.Status = PaymentStatus.Cancelled; + await _unitOfWork.GetRepository().Update(transaction); + await _unitOfWork.CommitAsync(); + } + else if (paymentInfo.status == "EXPIRED" && transaction.Status != PaymentStatus.Failed) + { + transaction.Status = PaymentStatus.Failed; + await _unitOfWork.GetRepository().Update(transaction); + await _unitOfWork.CommitAsync(); + } + + return new PaymentResponse + { + Success = true, + Message = "Payment success handled successfully", + TransactionId = orderCode + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling payment success for orderCode: {OrderCode}", orderCode); + return new PaymentResponse + { + Success = false, + Message = $"Error handling payment success: {ex.Message}" + }; + } + } + public async Task CancelPaymentAsync(string orderCode) + { + try + { + var transaction = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: pt => pt.ExternalTransactionId == orderCode + ); + + if (transaction == null) + { + return new PaymentResponse + { + Success = false, + Message = "Transaction not found" + }; + } + + transaction.Status = PaymentStatus.Cancelled; + await _unitOfWork.GetRepository().Update(transaction); + await _unitOfWork.CommitAsync(); + + return new PaymentResponse + { + Success = true, + Message = "Payment cancelled successfully", + TransactionId = orderCode + }; + } + catch (Exception ex) + { + return new PaymentResponse + { + Success = false, + Message = $"Error cancelling payment: {ex.Message}" + }; + } + } + + public async Task VerifyIapAsync(VerifyIapRequest request) + { + try + { + _logger.LogInformation("Verifying IAP for user {UserId}, product {ProductId}, purchase {PurchaseId}", + request.UserId, request.ProductId, request.PurchaseId); + + // Check if this purchase has already been processed + var existingTransaction = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: pt => pt.ExternalTransactionId == request.PurchaseId + ); + + if (existingTransaction != null) + { + if (existingTransaction.Status == PaymentStatus.Completed) + { + return new PaymentResponse + { + Success = false, + Message = "This purchase has already been processed", + TransactionId = existingTransaction.ExternalTransactionId + }; + } + } + + // Verify the user exists + var user = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: u => u.Id == request.UserId + ); + + if (user == null) + { + return new PaymentResponse + { + Success = false, + Message = "User not found" + }; + } + + // Verify the user plan exists + var userPlan = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: up => up.Id == request.UserPlanId && up.UserId == request.UserId + ); + + if (userPlan == null) + { + return new PaymentResponse + { + Success = false, + Message = "User plan not found" + }; + } + + // Here you would typically validate the purchase with Google Play Store or Apple App Store + // For now, we'll assume the purchase is valid and create a transaction record + + // Create a new payment transaction for the IAP + var paymentTransaction = new PaymentTransaction + { + Amount = GetAmountByProductId(request.ProductId), // You'll need to implement this method + Currency = "USD", // or determine based on product + PaymentMethod = "IAP", + PaymentGateway = "GooglePlay", // or "AppStore" based on platform + UserId = request.UserId, + UserPlanId = request.UserPlanId, + Status = PaymentStatus.Completed, // Set as completed since IAP was successful + ExternalTransactionId = request.PurchaseId, + TransactionDate = DateTime.UtcNow + }; + + if (existingTransaction != null) + { + // Update existing transaction + existingTransaction.Status = PaymentStatus.Completed; + existingTransaction.TransactionDate = DateTime.UtcNow; + await _unitOfWork.GetRepository().Update(existingTransaction); + } + else + { + // Create new transaction + await _unitOfWork.GetRepository().Insert(paymentTransaction); + } + + // Update user plan status to active + userPlan.IsActive = true; + userPlan.StartDate = DateTime.UtcNow; + userPlan.EndDate = CalculateEndDate(request.ProductId, DateTime.UtcNow); // Calculate based on product type + userPlan.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(userPlan); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("IAP verification completed successfully for user {UserId}, purchase {PurchaseId}", + request.UserId, request.PurchaseId); + + return new PaymentResponse + { + Success = true, + Message = "IAP verification completed successfully", + TransactionId = request.PurchaseId + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error verifying IAP for user {UserId}, purchase {PurchaseId}", + request.UserId, request.PurchaseId); + + return new PaymentResponse + { + Success = false, + Message = $"Error verifying IAP: {ex.Message}" + }; + } + } + + private decimal GetAmountByProductId(string productId) + { + // Map product IDs to their respective amounts + return productId.ToLower() switch + { + "basic_plan_monthly" => 9.99m, + "basic_plan_yearly" => 99.99m, + "premium_plan_monthly" => 19.99m, + "premium_plan_yearly" => 199.99m, + _ => 0m + }; + } + + private DateTime CalculateEndDate(string productId, DateTime startDate) + { + // Calculate end date based on product type + return productId.ToLower() switch + { + var pid when pid.Contains("monthly") => startDate.AddMonths(1), + var pid when pid.Contains("yearly") => startDate.AddYears(1), + _ => startDate.AddMonths(1) // Default to monthly + }; + } + + //public async Task VerifyPaymentAsync(string orderCode) + //{ + // try + // { + // var paymentInfo = await _payOS.getPaymentLinkInformation(int.Parse(orderCode)); + + // if (paymentInfo == null) + // { + // return new PaymentResponse + // { + // Success = false, + // Message = "Payment not found" + // }; + // } + + // return new PaymentResponse + // { + // Success = true, + // Message = "Payment verified successfully", + // TransactionId = orderCode + // }; + // } + // catch (Exception ex) + // { + // return new PaymentResponse + // { + // Success = false, + // Message = $"Error verifying payment: {ex.Message}" + // }; + // } + //} + } + + // Webhook data model cho PayOS + public class PayOSWebhookData + { + public string code { get; set; } = null!; + public string desc { get; set; } = null!; + public PayOSWebhookDataDetail data { get; set; } = null!; + public string signature { get; set; } = null!; + } + + public class PayOSWebhookDataDetail + { + public int orderCode { get; set; } + public int amount { get; set; } + public string description { get; set; } = null!; + public string accountNumber { get; set; } = null!; + public string reference { get; set; } = null!; + public string transactionDateTime { get; set; } = null!; + public string currency { get; set; } = null!; + public string paymentLinkId { get; set; } = null!; + public string code { get; set; } = null!; + public string desc { get; set; } = null!; + public int counterAccountBankId { get; set; } + public string counterAccountBankName { get; set; } = null!; + public string counterAccountName { get; set; } = null!; + public string counterAccountNumber { get; set; } = null!; + public string virtualAccountName { get; set; } = null!; + public string virtualAccountNumber { get; set; } = null!; + public string status { get; set; } = null!; + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/PlanService.cs b/TaskGenix.BLL/Services/Implements/PlanService.cs index a03810f..48e2902 100644 --- a/TaskGenix.BLL/Services/Implements/PlanService.cs +++ b/TaskGenix.BLL/Services/Implements/PlanService.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using ClaimRequest.BLL.Services; +using TaskGenix.BLL.Services; using MapsterMapper; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; @@ -13,47 +13,48 @@ using TaskGenix.DAL.Data; using TaskGenix.Repositories.Interfaces; -namespace TaskGenix.BLL.Services.Implements +namespace TaskGenix.BLL.Services.Implements; + +public class PlanService : BaseService, IPlanService { - public class PlanService : BaseService, IPlanService + public PlanService(IUnitOfWork unitOfWork, ILogger logger, + IHttpContextAccessor httpContextAccessor, IMapper mapper) : base(unitOfWork, logger, httpContextAccessor, + mapper) { - public PlanService(IUnitOfWork unitOfWork, ILogger logger, IHttpContextAccessor httpContextAccessor, IMapper mapper) : base(unitOfWork, logger, httpContextAccessor, mapper) - { - } + } - public async Task> GetAllPlans() - { - var plans = await _unitOfWork.GetRepository() - .GetListAsync( - selector: plan => _mapper.Map(plan), - include: q => q.Include(p => p.Features) - .ThenInclude(pf => pf.Feature)); - return plans; - } + public async Task> GetAllPlans() + { + var plans = await _unitOfWork.GetRepository() + .GetListAsync( + plan => _mapper.Map(plan), + include: q => q.Include(p => p.Features) + .ThenInclude(pf => pf.Feature)); + return plans; + } - public async Task GetPlanById(Guid id) - { - var plan = await _unitOfWork.GetRepository() - .FirstOrDefaultAsync( - selector: plan => _mapper.Map(plan), - include: q => q.Include(p => p.Features) - .ThenInclude(pf => pf.Feature)) ?? throw new KeyNotFoundException($"Plan with ID {id} not found."); - return plan; - } + public async Task GetPlanById(Guid id) + { + var plan = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + plan => _mapper.Map(plan), + include: q => q.Include(p => p.Features) + .ThenInclude(pf => pf.Feature)) ?? throw new KeyNotFoundException($"Plan with ID {id} not found."); + return plan; + } - public Task CreatePlan(PlanResponse plan) - { - throw new NotImplementedException(); - } + public Task CreatePlan(PlanResponse plan) + { + throw new NotImplementedException(); + } - public Task DeletePlan(Guid id) - { - throw new NotImplementedException(); - } + public Task DeletePlan(Guid id) + { + throw new NotImplementedException(); + } - public Task UpdatePlan(Guid id, PlanResponse plan) - { - throw new NotImplementedException(); - } + public Task UpdatePlan(Guid id, PlanResponse plan) + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/ProjectMemberService.cs b/TaskGenix.BLL/Services/Implements/ProjectMemberService.cs new file mode 100644 index 0000000..6dc5716 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/ProjectMemberService.cs @@ -0,0 +1,180 @@ +using Mapster; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.Core.Requests.ProjectMember; +using TaskGenix.Core.Responses.Project; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements; + +public class ProjectMemberService : BaseService, IProjectMemberService +{ + public ProjectMemberService(IUnitOfWork unitOfWork, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + } + + #region Include Strategies + + /// + /// Standard includes for project members - includes User data + /// + private Func, IQueryable> ApplyMemberIncludes() + { + return query => query + .Include(pm => pm.User) + .AsNoTracking(); + } + + #endregion + + #region Public Methods + + public async Task> GetMembersByProjectId(Guid projectId) + { + return await _unitOfWork.GetRepository().GetListAsync( + pm => pm.Adapt(), + pm => pm.ProjectId == projectId, + include: ApplyMemberIncludes()); + } + + #endregion + + public async Task AddMemberToProject(Guid projectId, AddProjectMemberRequest request) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + // Verify project exists + var project = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: p => p.Id == projectId); + if (project == null) throw new NotFoundException("Project not found"); + + // TODO: chưa sài JWT + // var currentUserId = GetCurrentUserId(); + // if (project.OwnerId != currentUserId) + // throw new ForbiddenException("Only project owner can add members to this project"); + + // Check if user is already a member + var existingMember = await GetProjectMember(projectId, request.UserId); + if (existingMember != null) + throw new ConflictException("User is already a member of this project"); + + // Verify user exists + var user = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Id == request.UserId); + if (user == null) throw new NotFoundException("User not found"); + + var projectMember = new ProjectMember + { + ProjectId = projectId, + UserId = request.UserId, + Role = request.Role, + JoinedAt = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().Insert(projectMember); + await _unitOfWork.CommitAsync(); + + // Return the created member with user data + var createdMember = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + pm => pm.Adapt(), + pm => pm.ProjectId == projectId && pm.UserId == request.UserId, + include: ApplyMemberIncludes()); + + return createdMember == null ? throw new BusinessException("Failed to retrieve created project member") : createdMember; + }); + } + + public async Task UpdateProjectMember(Guid projectId, Guid userId, + UpdateProjectMemberRequest request) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + // Verify project exists and check authorization + var project = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: p => p.Id == projectId); + if (project == null) throw new NotFoundException("Project not found"); + + //TODO: chưa sài JWT + // var currentUserId = GetCurrentUserId(); + // if (project.OwnerId != currentUserId) + // throw new ForbiddenException("Only project owner can update member roles"); + + var checkMember = await IsUserMemberOfProject(projectId, userId); + + if (checkMember == null) + throw new NotFoundException("Project member not found"); + + checkMember.Role = request.Role; + checkMember.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(checkMember); + await _unitOfWork.CommitAsync(); + + // Return the updated member with user data + var updatedMember = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + pm => pm.Adapt(), + pm => pm.ProjectId == projectId && pm.UserId == userId, + include: ApplyMemberIncludes()); + + return updatedMember == null ? throw new BusinessException("Failed to retrieve updated project member") : updatedMember; + }); + } + + public async Task RemoveMemberFromProject(Guid projectId, Guid userId) + { + await _unitOfWork.ExecuteInTransactionAsync(async () => + { + // Verify project exists and check authorization + var project = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: p => p.Id == projectId); + if (project == null) throw new NotFoundException("Project not found"); + + //TODO: chưa sài JWT + // var currentUserId = GetCurrentUserId(); + // if (project.OwnerId != currentUserId) + // throw new ForbiddenException("Only project owner can remove members from this project"); + + var checkMember = await IsUserMemberOfProject(projectId, userId); + + if (checkMember == null) + throw new NotFoundException("Project member not found"); + + // Prevent removing the project owner + if (project.OwnerId == userId) + throw new BusinessException("Cannot remove the project owner from project members"); + + await _unitOfWork.GetRepository().Delete(checkMember); + await _unitOfWork.CommitAsync(); + }); + } + + #region Private Helper Methods + + + private async Task GetProjectMember(Guid projectId, Guid userId) + { + return await _unitOfWork.GetRepository().FirstOrDefaultAsync( + pm => pm, + pm => pm.ProjectId == projectId && pm.UserId == userId, + include: ApplyMemberIncludes()); + } + + + private async Task IsUserMemberOfProject(Guid projectId, Guid userId) + { + return await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: pm => pm.ProjectId == projectId && pm.UserId == userId); + } + + #endregion +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/ProjectProgressCalculator.cs b/TaskGenix.BLL/Services/Implements/ProjectProgressCalculator.cs new file mode 100644 index 0000000..4f14489 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/ProjectProgressCalculator.cs @@ -0,0 +1,67 @@ +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; + +namespace TaskGenix.BLL.Services.Implements; + +public class ProjectProgressCalculator : IProjectProgressCalculator +{ + public decimal CalculateProgress(IEnumerable tasks) + { + if (!tasks.Any()) + return 0m; + + var completedTasks = tasks.Count(t => t.Status == TaskGenStatus.Completed); + return Math.Round((decimal)completedTasks / tasks.Count() * 100, 2); + } + + public decimal CalculateWeightedProgress(IEnumerable tasks) + { + if (!tasks.Any()) + return 0m; // Empty workspace = 0% progress + + var totalWeight = 0m; + var completedWeight = 0m; + + foreach (var task in tasks) + { + var weight = GetTaskWeight(task.Priority); + totalWeight += weight; + + // Calculate progress based on task status + var taskProgress = GetTaskStatusProgress(task.Status); + + // Consider overdue tasks + if (task.Status != TaskGenStatus.Completed && task.DueDate.HasValue && task.DueDate < DateTime.UtcNow) + // Overdue tasks get penalty in progress calculation + taskProgress *= 0.8m; // 20% penalty for overdue + + completedWeight += weight * taskProgress; + } + + return totalWeight > 0 ? Math.Round(completedWeight / totalWeight * 100, 2) : 0m; + } + + private static decimal GetTaskStatusProgress(TaskGenStatus status) + { + return status switch + { + TaskGenStatus.Completed => 1.0m, // 100% + TaskGenStatus.InProgress => 0.6m, // 60% + TaskGenStatus.Pending => 0.1m, // 10% (just started) + TaskGenStatus.Overdue => 0.3m, // 30% (some work done but overdue) + _ => 0m + }; + } + + private static decimal GetTaskWeight(TaskGenPriority priority) + { + return priority switch + { + TaskGenPriority.Critical => 4m, + TaskGenPriority.High => 3m, + TaskGenPriority.Medium => 2m, + TaskGenPriority.Low => 1m, + _ => 1m + }; + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/ProjectService.cs b/TaskGenix.BLL/Services/Implements/ProjectService.cs index a621207..61f5a03 100644 --- a/TaskGenix.BLL/Services/Implements/ProjectService.cs +++ b/TaskGenix.BLL/Services/Implements/ProjectService.cs @@ -1,108 +1,273 @@ -using ClaimRequest.BLL.Services; -using Mapster; -using MapsterMapper; +using MapsterMapper; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using TaskGenix.BLL.Services.Interfaces; using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.Project; using TaskGenix.Core.Responses.Project; -using TaskGenix.Core.Responses.TaskGen; using TaskGenix.DAL.Data; using TaskGenix.Repositories.Interfaces; -namespace TaskGenix.BLL.Services.Implements +namespace TaskGenix.BLL.Services.Implements; + +public class ProjectService : BaseService, IProjectService { - public class ProjectService : BaseService, IProjectService + private readonly IProjectProgressCalculator _progressCalculator; + + public ProjectService(IUnitOfWork unitOfWork, ILogger logger, + IHttpContextAccessor httpContextAccessor, IMapper mapper, IProjectProgressCalculator progressCalculator) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + _progressCalculator = progressCalculator; + } + + private Func, IQueryable> ApplyProjectListIncludes(bool asNoTracking = true) { - public ProjectService(IUnitOfWork unitOfWork, ILogger logger, - IHttpContextAccessor httpContextAccessor, IMapper mapper) - : base(unitOfWork, logger, httpContextAccessor, mapper) + return query => { + var q = query + .Include(p => p.Members) + .Include(p => p.TaskGens).ThenInclude(t => t.ResourceLinks); - } + return asNoTracking ? q.AsNoTracking() : q; + }; + } - public async Task> GetAllProjects() + /// + /// Full includes for detail views - includes all nested data + /// Use only when full data is absolutely needed (e.g., GetProjectById) + /// + private Func, IQueryable> ApplyProjectDetailIncludes(bool asNoTracking = true) + { + return query => { - return await _unitOfWork.GetRepository().GetListAsync(); - } + var q = query + .Include(p => p.Members).ThenInclude(pm => pm.User) + .Include(p => p.TaskGens).ThenInclude(t => t.ResourceLinks) + .Include(p => p.TaskGens).ThenInclude(t => t.Assignments) + .ThenInclude(ta => ta.AssignedUser); - public async Task GetProjectById(Guid projectId) - { - return await _unitOfWork.GetRepository().SingleOrDefaultAsync( - predicate: p => p.Id == projectId, - include: query => query - .Include(p => p.Members) - .Include(p => p.TaskGens) - .AsNoTracking()) - ?? throw new KeyNotFoundException($"Project with ID {projectId} not found."); - } - - public async Task> GetProjectsByOwnerId(Guid ownerId) - { - return await _unitOfWork.GetRepository().GetListAsync(predicate: p => p.OwnerId == ownerId); - } + return asNoTracking ? q.AsNoTracking() : q; + }; + } - public async Task> GetProjectsByUserId(Guid userId) - { - return await _unitOfWork.GetRepository().GetListAsync( - predicate: p => p.Members.Any(m => m.UserId == userId)); - } + private ProjectResponse MapProjectToResponse(Project project) + { + var response = _mapper.Map(project); + response.Progress = _progressCalculator.CalculateWeightedProgress(project.TaskGens ?? new List()); + return response; + } - public Task CreateProject(Project project) - { - throw new NotImplementedException(); - } + private IEnumerable MapProjectsToResponse(IEnumerable projects) + { + return projects.Select(MapProjectToResponse); + } - public Task DeleteProject(Guid projectId) - { - throw new NotImplementedException(); - } + public async Task> GetAllProjects() + { + var projects = await _unitOfWork.GetRepository() + .GetListAsync( + p => p, + include: ApplyProjectListIncludes()); - public Task UpdateProject(Project project) - { - throw new NotImplementedException(); - } + return MapProjectsToResponse(projects); + } + + public async Task GetProjectById(Guid projectId) + { + var project = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: p => p.Id == projectId && p.IsActive, + include: ApplyProjectDetailIncludes()) ?? + throw new KeyNotFoundException($"Project with ID {projectId} not found."); + _logger.LogInformation("Project {ProjectId} has {TaskCount} TaskGens with resources", + projectId, project.TaskGens?.Count ?? 0); + + return MapProjectToResponse(project); + } + + public async Task> GetProjectsByOwnerId(Guid ownerId) + { + var projects = await _unitOfWork.GetRepository() + .GetListAsync( + p => p, + p => p.OwnerId == ownerId && p.IsActive, + include: ApplyProjectListIncludes()); + + return MapProjectsToResponse(projects); + } + + public async Task> GetProjectsByOwnerIdAndStatus(Guid ownerId, ProjectStatus status) + { + var projects = await _unitOfWork.GetRepository() + .GetListAsync( + p => p, + p => p.OwnerId == ownerId && p.Status == status && p.IsActive, + include: ApplyProjectListIncludes()); + + return MapProjectsToResponse(projects); + } + + public async Task> GetProjectsByUserId(Guid userId) + { - // Method using Mapster to get ProjectResponse DTOs - public async Task> GetAllProjectsDto() + var projects = await _unitOfWork.GetRepository() + .GetListAsync( + p => p, + p => p.Members.Any(m => m.UserId == userId) && p.IsActive, + p => p.OrderByDescending(pr => pr.CreatedAt), + include: ApplyProjectListIncludes()); + + return MapProjectsToResponse(projects); + } + + public async Task CreateProject(CreateProjectRequest request) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => { - return await _unitOfWork.GetRepository().GetListAsync( - selector: project => project.Adapt() - ); - } + var owner = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Id == request.OwnerId) + ?? throw new KeyNotFoundException($"User with ID {request.OwnerId} not found"); + + var project = _mapper.Map(request); + project.CreatedAt = DateTime.UtcNow; + project.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Insert(project); + + var memberIds = (request.MemberIds ?? Enumerable.Empty()).ToHashSet(); + + //Dam bao owner + memberIds.Add(request.OwnerId); + + var members = new List(); + foreach (var memberId in memberIds) + { + var memberUser = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: u => u.Id == memberId); + if (memberUser != null) + members.Add(new ProjectMember + { + ProjectId = project.Id, + UserId = memberId, + Role = memberId == request.OwnerId ? MemberRole.Owner : MemberRole.Member, + JoinedAt = DateTime.UtcNow + }); + else + _logger.LogWarning("Member user {UserId} not found while creating project {ProjectId}", memberId, + project.Id); + } - public async Task> GetProjectsByOwnerIdDto(Guid ownerId) + if (members.Any()) + await _unitOfWork.GetRepository().InsertRange(members); + + // Link tasks if present + if (request.TaskGenIds != null && request.TaskGenIds.Any()) + foreach (var taskId in request.TaskGenIds.Distinct()) + { + var task = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: t => t.Id == taskId); + if (task != null) + { + task.ProjectId = project.Id; + task.UpdatedAt = DateTime.UtcNow; + await _unitOfWork.GetRepository().Update(task); + } + else + { + _logger.LogWarning("TaskGen {TaskId} not found while creating project {ProjectId}", taskId, + project.Id); + } + } + + await _unitOfWork.CommitAsync(); + + var projectWithData = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: p => p.Id == project.Id, + include: ApplyProjectDetailIncludes()); + + return MapProjectToResponse(projectWithData!); + }); + } + + public async Task UpdateProject(Guid projectId, UpdateProjectRequest request) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => { - return await _unitOfWork.GetRepository().GetListAsync( - selector: project => project.Adapt(), - predicate: p => p.OwnerId == ownerId - ); - } + var project = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: p => p.Id == projectId); + + if (project == null) + throw new KeyNotFoundException($"Project with ID {projectId} not found"); - public async Task> GetProjectsByUserIdDto(Guid userId) + // Update only project-level properties + project = _mapper.Map(request, project); + project.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(project); + await _unitOfWork.CommitAsync(); + + // Fetch updated project with detail includes (needed for response) + var projectWithData = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: p => p.Id == projectId, + include: ApplyProjectDetailIncludes()); + + return MapProjectToResponse(projectWithData!); + }); + } + + public async Task DeleteProject(Guid projectId) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => { - return await _unitOfWork.GetRepository().GetListAsync( - selector: project => project.Adapt(), - predicate: p => p.Members.Any(pm => pm.UserId == userId) - ); - } + var project = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: p => p.Id == projectId); + if (project == null) + throw new KeyNotFoundException($"Project with ID {projectId} not found."); + + project.IsActive = false; + project.UpdatedAt = DateTime.UtcNow; + await _unitOfWork.GetRepository().Update(project); + + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Soft deleted project {ProjectId} (IsActive = false).", projectId); - public async Task GetProjectByIdDto(Guid projecctId) + return true; + }); + } + + // Progress-specific methods for better performance + public async Task GetProjectProgress(Guid projectId) + { + var tasks = await _unitOfWork.GetRepository() + .GetListAsync( + predicate: t => t.ProjectId == projectId, + include: query => query.AsNoTracking()); + + return new ProjectProgressResponse { - return await _unitOfWork.GetRepository().FirstOrDefaultAsync( - selector: project => project.Adapt(), - predicate: p => p.Id == projecctId, - include: query => query - .Include(p => p.Members) - .ThenInclude(m => m.User) - .Include(p => p.TaskGens) - .AsNoTracking()) - ?? throw new KeyNotFoundException($"Project with ID {projecctId} not found."); - } + ProjectId = projectId, + Progress = _progressCalculator.CalculateWeightedProgress(tasks), + TotalTasks = tasks.Count(), + CompletedTasks = tasks.Count(t => t.Status == TaskGenStatus.Completed), + InProgressTasks = tasks.Count(t => t.Status == TaskGenStatus.InProgress), + TasksByPriority = tasks.GroupBy(t => t.Priority) + .ToDictionary(g => g.Key, g => g.Count()), + TasksByStatus = tasks.GroupBy(t => t.Status) + .ToDictionary(g => g.Key, g => g.Count()) + }; + } + + public async Task> GetProjectsProgressByUserId(Guid userId) + { + var projectIds = await _unitOfWork.GetRepository() + .GetListAsync( + pm => pm.ProjectId, + pm => pm.UserId == userId, + include: query => query.AsNoTracking()); + + var progressTasks = projectIds.Select(projectId => GetProjectProgress(projectId)); + return await Task.WhenAll(progressTasks); } } \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/RecordingService.cs b/TaskGenix.BLL/Services/Implements/RecordingService.cs new file mode 100644 index 0000000..a67a0d1 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/RecordingService.cs @@ -0,0 +1,285 @@ +using Amazon.S3; +using Amazon.S3.Model; +using TaskGenix.BLL.Services; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.DAL.Data; +using TaskGenix.DAL.Repositories.Implements; +using TaskGenix.DAL.Repositories.Interfaces; +using TaskGenix.Core.Requests.Recording; +using TaskGenix.Core.Responses.Recording; +using TaskGenix.Repositories.Interfaces; +using System.Security.Cryptography; + +public class RecordingService : BaseService, IRecordingService +{ + private readonly IAmazonS3 _s3; + private readonly IConfiguration _config; public RecordingService(IUnitOfWork unitOfWork, +ILogger logger, IHttpContextAccessor httpContextAccessor, +IMapper mapper, IAmazonS3 s3, IConfiguration config) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + _s3 = s3; + _config = config; + } + + private string GetFileExtension(string? mimeType) + { + return mimeType switch + { + "audio/mpeg" => ".mp3", + "audio/mp4" => ".m4a", + "audio/x-m4a" => ".m4a", + "audio/wav" => ".wav", + "audio/x-wav" => ".wav", + "audio/flac" => ".flac", + "audio/ogg" => ".ogg", + "audio/webm" => ".webm", + "audio/aac" => ".aac", + "audio/x-aac" => ".aac", + _ => ".audio" + }; + } + + /// Sanitize email để đưa vào object key (tránh ký tự lạ) + private string SanitizeOwnerEmail(string email) + { + return email.Replace("@", "_at_").Replace(".", "_dot_"); + } + + // ========== LUU INTERNAL PATH, DÙNG PRESIGNED URL ========== + public async Task UploadRecordingAsync( + Stream recordingStream, // Nhận Stream + long contentLength, // Nhận độ dài file + string title, + string ownerEmail, + string? mimeType, + TimeSpan? duration, + string? transcriptText = null) + { + var bucket = _config["CloudflareR2:BucketName"] ?? throw new InvalidOperationException("Cloudflare R2 bucket not configured"); + var safeOwner = SanitizeOwnerEmail(ownerEmail); + var ext = GetFileExtension(mimeType); + var fileName = $"{Guid.NewGuid()}{ext}"; + var key = $"audio/{safeOwner}/{fileName}"; + + try + { + using var memoryStream = new MemoryStream(); + await recordingStream.CopyToAsync(memoryStream); + var fileBytes = memoryStream.ToArray(); + + var getPresignedUrlRequest = new GetPreSignedUrlRequest + { + BucketName = bucket, + Key = key, + Verb = HttpVerb.PUT, + Expires = DateTime.UtcNow.AddMinutes(15), + ContentType = mimeType ?? "application/octet-stream" + }; + + string presignedUrl = _s3.GetPreSignedURL(getPresignedUrlRequest); + + _logger.LogInformation("Generated presigned URL for upload: {Url}", presignedUrl); + + using var httpClient = new HttpClient(); + using var requestContent = new ByteArrayContent(fileBytes); + + requestContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mimeType ?? "application/octet-stream"); + + var response = await httpClient.PutAsync(presignedUrl, requestContent); + response.EnsureSuccessStatusCode(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to upload to R2 using presigned URL. Error: {ErrorMessage}", ex.Message); + throw new InvalidOperationException("Failed to upload recording file", ex); + } + + // Tìm OwnerId từ email + var user = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: u => u.Email == ownerEmail); + + // Chỉ lưu key, không lưu public URL + var recording = new Recording + { + Title = title, + ObjectKey = key, + Duration = duration, + FileSize = contentLength, + MimeType = mimeType, + UploadedAt = DateTimeOffset.UtcNow, + OwnerId = user?.Id, + OwnerEmail = ownerEmail, + IsTranscriptRequested = !string.IsNullOrEmpty(transcriptText), + TranscriptStatus = string.IsNullOrEmpty(transcriptText) + ? TranscriptStatus.None + : TranscriptStatus.Success, + TranscriptText = transcriptText + }; + + try + { + await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var success = await _unitOfWork.GetRepository().Insert(recording); + if (!success) + { + throw new InvalidOperationException("Failed to insert recording to database"); + } + return success; + }); + } + catch (Exception ex) + { + // cleanup file nếu DB fail + try + { + await _s3.DeleteObjectAsync(bucket, key); + } + catch + { + // log cleanup fail, không throw + } + throw new InvalidOperationException("Failed to save recording metadata", ex); + } + + return _mapper.Map(recording); + } + + + + public async Task GetRecordingByIdAsync(Guid recordingId) + { + var recording = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: r => r.Id == recordingId, + include: query => query.AsNoTracking()) ?? throw new NotFoundException($"Recording with ID {recordingId} not found"); + + return recording; + } + + public async Task GetRecordingMetaAsync(Guid recordingId) + { + return await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: r => r.Id == recordingId, + include: query => query.AsNoTracking()); + } + + public async Task> GetAllRecordings() + { + var recordings = await _unitOfWork.GetRepository() + .GetListAsync(include: query => query.AsNoTracking()); + return _mapper.Map>(recordings); + } + + public async Task GetRecordingResponseByIdAsync(Guid recordingId) + { + var recording = await GetRecordingByIdAsync(recordingId); + return _mapper.Map(recording); + } + + public async Task CreateRecording(CreateRecordingRequest request) + { + // Tìm OwnerId từ email nếu chưa có + Guid? ownerId = request.OwnerId; + if (string.IsNullOrEmpty(request.OwnerEmail) && ownerId == null) + { + throw new InvalidOperationException("OwnerEmail or OwnerId must be provided"); + } + if (ownerId == null && !string.IsNullOrEmpty(request.OwnerEmail)) + { + var user = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: u => u.Email == request.OwnerEmail); + ownerId = user?.Id; + } + + var recording = _mapper.Map(request); + recording.OwnerId = ownerId; + recording.UploadedAt = DateTimeOffset.UtcNow; + recording.TranscriptStatus = TranscriptStatus.None; + + await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var success = await _unitOfWork.GetRepository().Insert(recording); + if (!success) + { + throw new InvalidOperationException("Failed to insert recording to database"); + } + return success; + }); + + return _mapper.Map(recording); + } + + public async Task UpdateRecording(Guid recordingId, UpdateRecordingRequest request) + { + var recording = await GetRecordingByIdAsync(recordingId); + + // Nếu có OwnerEmail, tìm OwnerId + if (!string.IsNullOrEmpty(request.OwnerEmail)) + { + var user = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: u => u.Email == request.OwnerEmail); + if (user != null) + { + request.OwnerId = user.Id; + } + } + + _mapper.Map(request, recording); + + await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var success = await _unitOfWork.GetRepository().Update(recording); + if (!success) + { + throw new InvalidOperationException("Failed to update recording"); + } + return success; + }); + + return _mapper.Map(recording); + } + + public async Task DeleteRecording(Guid recordingId) + { + var recording = await GetRecordingByIdAsync(recordingId); + var bucket = _config["CloudflareR2:BucketName"] ?? throw new InvalidOperationException("Cloudflare R2 bucket not configured"); + + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var success = await _unitOfWork.GetRepository().Delete(recording); + if (success) + { + // Xóa file trên R2 + try + { + await _s3.DeleteObjectAsync(bucket, recording.ObjectKey); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete recording file from R2: {ObjectKey}", recording.ObjectKey); + // Không throw, vì DB đã xóa + } + } + return success; + }); + } + + public async Task> GetRecordingsByUserIdAsync(Guid userId) + { + var recordings = await _unitOfWork.GetRepository() + .GetListAsync( + predicate: r => r.OwnerId == userId, + include: query => query.AsNoTracking()); + return _mapper.Map>(recordings); + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/SystemStatusService.cs b/TaskGenix.BLL/Services/Implements/SystemStatusService.cs new file mode 100644 index 0000000..514e4b1 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/SystemStatusService.cs @@ -0,0 +1,483 @@ +using System.Diagnostics; +using Amazon.S3; +using Amazon.S3.Model; +using FirebaseAdmin.Messaging; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Responses.SystemStatus; +using TaskGenix.DAL.Data; + +namespace TaskGenix.BLL.Services.Implements; + +/// +/// Provides real-time health status for core system services. +/// Includes caching, timing metrics, and automatic alert notifications. +/// +public class SystemStatusService : ISystemStatusService +{ + private readonly ILogger _logger; + private readonly TaskGenixDbContext _dbContext; + private readonly IEmailService _emailService; + private readonly IAmazonS3 _s3Client; + private readonly IConfiguration _configuration; + + // cache to prevent spamming health checks + private static DateTime _lastCheckTime = DateTime.MinValue; + private static SystemStatusResponse? _cachedResponse; + private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30); + + private static readonly DateTime SystemStartTime = DateTime.UtcNow; + + public SystemStatusService( + ILogger logger, + TaskGenixDbContext dbContext, + IEmailService emailService, + IAmazonS3 s3Client, + IConfiguration configuration) + { + _logger = logger; + _dbContext = dbContext; + _emailService = emailService; + _s3Client = s3Client; + _configuration = configuration; + } + + public async Task GetSystemStatusAsync() + { + if (_cachedResponse != null && (DateTime.UtcNow - _lastCheckTime) < CacheDuration) + { + _logger.LogDebug("Returning cached system status result"); + return _cachedResponse; + } + + _logger.LogInformation("Performing full system health check..."); + + var services = new List + { + await CheckDatabaseStatusAsync(), + await CheckAuthenticationStatusAsync(), + await CheckEmailNotificationStatusAsync(), + await CheckApiStatusAsync(), + await CheckIntegrationsStatusAsync(), + await CheckReportingStatusAsync() + }; + + var overallStatus = DetermineOverallStatus(services); + + var response = new SystemStatusResponse + { + OverallStatus = overallStatus, + CheckedAt = DateTime.UtcNow, + Services = services, + Metadata = new SystemMetadata + { + Environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production", + } + }; + + // alert admin if severe issues detected + if (overallStatus.Contains("Major", StringComparison.OrdinalIgnoreCase)) + { + _logger.LogWarning("System experiencing major issues! Triggering alert email."); + await _emailService.SendNotificationEmailAsync( + "admin@taskgenix.example", + "🚨 TaskGenix System Alert", + $"

⚠️ Major System Outage Detected

" + + $"

As of {DateTime.UtcNow:u}, one or more critical services are down.

" + + $"

Please check the system dashboard immediately.

" + ); + } + + // update cache + _cachedResponse = response; + _lastCheckTime = DateTime.UtcNow; + + return response; + } + + public async Task CheckServiceStatusAsync(string serviceName) + { + _logger.LogInformation("Checking status for service: {ServiceName}", serviceName); + + return serviceName.ToLowerInvariant() switch + { + "database" => await CheckDatabaseStatusAsync(), + "authentication" => await CheckAuthenticationStatusAsync(), + "email" => await CheckEmailNotificationStatusAsync(), + "api" => await CheckApiStatusAsync(), + "integrations" => await CheckIntegrationsStatusAsync(), + "reporting" => await CheckReportingStatusAsync(), + _ => new ServiceStatusResponse + { + ServiceName = serviceName, + Status = ServiceHealthStatus.MinorOutage, + Message = "Service not found", + LastChecked = DateTime.UtcNow + } + }; + } + + public Task> GetAvailableServicesAsync() + { + var services = new List + { + "Database", + "Authentication", + "Email Notifications", + "Main Platform API", + "Integrations Service", + "Reporting & Analytics" + }; + + return Task.FromResult(services); + } + + #region Private Health Check Methods + + private async Task CheckDatabaseStatusAsync() + { + var sw = Stopwatch.StartNew(); + try + { + var canConnect = await _dbContext.Database.CanConnectAsync(); + sw.Stop(); + + if (canConnect) + { + var userCount = await _dbContext.Users.CountAsync(); + return new ServiceStatusResponse + { + ServiceName = "Database", + Status = ServiceHealthStatus.Operational, + Message = "Database connection successful", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds, + Version = "PostgreSQL", + AdditionalInfo = new Dictionary + { + ["DBType"] = "PostgreSQL", + ["UserCount"] = userCount + } + }; + } + + return new ServiceStatusResponse + { + ServiceName = "Database", + Status = ServiceHealthStatus.MajorOutage, + Message = "Cannot connect to database", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds + }; + } + catch (Exception ex) + { + sw.Stop(); + _logger.LogError(ex, "Database health check failed"); + return new ServiceStatusResponse + { + ServiceName = "Database", + Status = ServiceHealthStatus.MajorOutage, + Message = $"Database error: {ex.Message}", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds + }; + } + } + + private Task CheckAuthenticationStatusAsync() + { + var sw = Stopwatch.StartNew(); + try + { + // Assume JWT/OTP auth is configured + sw.Stop(); + return Task.FromResult(new ServiceStatusResponse + { + ServiceName = "Authentication & Login", + Status = ServiceHealthStatus.Operational, + Message = "Authentication service operational", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds, + Version = "JWT + OTP", + AdditionalInfo = new Dictionary + { + ["Provider"] = "JWT + OTP", + ["TokenType"] = "Bearer" + } + }); + } + catch (Exception ex) + { + sw.Stop(); + _logger.LogError(ex, "Authentication health check failed"); + return Task.FromResult(new ServiceStatusResponse + { + ServiceName = "Authentication & Login", + Status = ServiceHealthStatus.MajorOutage, + Message = $"Authentication error: {ex.Message}", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds + }); + } + } + + private Task CheckEmailNotificationStatusAsync() + { + var sw = Stopwatch.StartNew(); + try + { + sw.Stop(); + return Task.FromResult(new ServiceStatusResponse + { + ServiceName = "Email Notifications", + Status = ServiceHealthStatus.Operational, + Message = "Email service configured and ready", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds, + Version = "SMTP", + AdditionalInfo = new Dictionary + { + ["Provider"] = "SMTP", + ["Status"] = "Ready" + } + }); + } + catch (Exception ex) + { + sw.Stop(); + _logger.LogError(ex, "Email service health check failed"); + return Task.FromResult(new ServiceStatusResponse + { + ServiceName = "Email Notifications", + Status = ServiceHealthStatus.Degraded, + Message = $"Email service issue: {ex.Message}", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds + }); + } + } + + private Task CheckApiStatusAsync() + { + var sw = Stopwatch.StartNew(); + sw.Stop(); + + return Task.FromResult(new ServiceStatusResponse + { + ServiceName = "Main Platform API", + Status = ServiceHealthStatus.Operational, + Message = "API service operational", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds, + Version = "v1.0", + AdditionalInfo = new Dictionary + { + ["Environment"] = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production" + } + }); + } + + private Task CheckIntegrationsStatusAsync() + { + var sw = Stopwatch.StartNew(); + + // Check các integration services song song + var tasks = new List> + { + CheckFirebaseAsync(), + CheckCloudflareR2Async(), + CheckPayOSAsync() + }; + + try + { + Task.WaitAll(tasks.ToArray(), TimeSpan.FromSeconds(10)); + sw.Stop(); + + var results = tasks.Select(t => t.Result).ToList(); + var allHealthy = results.All(r => r.IsHealthy); + var someIssues = results.Any(r => !r.IsHealthy); + + var additionalInfo = new Dictionary(); + + foreach (var result in results) + { + additionalInfo[result.Name] = new + { + Status = result.IsHealthy ? "Operational" : "Issues", + ResponseTimeMs = result.Ms, + Details = result.Details + }; + } + + return Task.FromResult(new ServiceStatusResponse + { + ServiceName = "Integrations Service", + Status = allHealthy ? ServiceHealthStatus.Operational : + someIssues ? ServiceHealthStatus.MinorOutage : ServiceHealthStatus.MajorOutage, + Message = allHealthy ? "All third-party integrations operational" : + "Some third-party integrations experiencing issues", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds, + Version = "v1.0", + AdditionalInfo = additionalInfo + }); + } + catch (Exception ex) + { + sw.Stop(); + _logger.LogError(ex, "Integrations health check failed"); + return Task.FromResult(new ServiceStatusResponse + { + ServiceName = "Integrations Service", + Status = ServiceHealthStatus.MajorOutage, + Message = $"Integration error: {ex.Message}", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds + }); + } + } + + /// + /// Check Firebase connectivity by attempting to validate a test token structure + /// + private async Task<(string Name, bool IsHealthy, long Ms, string Details)> CheckFirebaseAsync() + { + var sw = Stopwatch.StartNew(); + try + { + // Test Firebase Admin SDK availability + var firebaseApp = FirebaseAdmin.FirebaseApp.DefaultInstance; + + if (firebaseApp == null) + { + sw.Stop(); + return ("Firebase", false, sw.ElapsedMilliseconds, "Firebase app not initialized"); + } + + // Simple connectivity test - check if we can access messaging + var messaging = FirebaseMessaging.DefaultInstance; + sw.Stop(); + + return ("Firebase", true, sw.ElapsedMilliseconds, "Operational"); + } + catch (Exception ex) + { + sw.Stop(); + _logger.LogWarning(ex, "Firebase health check failed"); + return ("Firebase", false, sw.ElapsedMilliseconds, $"Error: {ex.Message}"); + } + } + + /// + /// Check Cloudflare R2 (S3) connectivity + /// + private async Task<(string Name, bool IsHealthy, long Ms, string Details)> CheckCloudflareR2Async() + { + var sw = Stopwatch.StartNew(); + try + { + var bucketName = _configuration["CloudflareR2:BucketName"]; + + if (string.IsNullOrEmpty(bucketName)) + { + sw.Stop(); + return ("CloudflareR2", false, sw.ElapsedMilliseconds, "Bucket not configured"); + } + + // List buckets as a simple connectivity test + var request = new ListObjectsV2Request + { + BucketName = bucketName, + MaxKeys = 1 // Only fetch 1 object for speed + }; + + var response = await _s3Client.ListObjectsV2Async(request); + sw.Stop(); + + return ("CloudflareR2", true, sw.ElapsedMilliseconds, $"Operational ({response.KeyCount} objects sample)"); + } + catch (Exception ex) + { + sw.Stop(); + _logger.LogWarning(ex, "Cloudflare R2 health check failed"); + return ("CloudflareR2", false, sw.ElapsedMilliseconds, $"Error: {ex.Message}"); + } + } + + /// + /// Check PayOS API connectivity (simulated - you can enhance this) + /// + private async Task<(string Name, bool IsHealthy, long Ms, string Details)> CheckPayOSAsync() + { + var sw = Stopwatch.StartNew(); + try + { + var clientId = _configuration["PayOS:ClientId"]; + var apiKey = _configuration["PayOS:ApiKey"]; + + if (string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(apiKey)) + { + sw.Stop(); + return ("PayOS", false, sw.ElapsedMilliseconds, "PayOS not configured"); + } + + // TODO: You can add actual PayOS API health check here + // For now, just verify configuration exists + await Task.Delay(10); // Simulate API call + sw.Stop(); + + // Simulating minor issues for demo + return ("PayOS", false, sw.ElapsedMilliseconds, "Minor Issues - Payment gateway delays reported"); + } + catch (Exception ex) + { + sw.Stop(); + _logger.LogWarning(ex, "PayOS health check failed"); + return ("PayOS", false, sw.ElapsedMilliseconds, $"Error: {ex.Message}"); + } + } + + private Task CheckReportingStatusAsync() + { + var sw = Stopwatch.StartNew(); + sw.Stop(); + + return Task.FromResult(new ServiceStatusResponse + { + ServiceName = "Reporting & Analytics", + Status = ServiceHealthStatus.Maintenance, + Message = "Scheduled maintenance underway", + LastChecked = DateTime.UtcNow, + ResponseTimeMs = sw.ElapsedMilliseconds, + Version = "v1.0", + AdditionalInfo = new Dictionary + { + ["MaintenanceWindow"] = "Ongoing", + ["ExpectedCompletion"] = "Within 2 hours" + } + }); + } + + private string DetermineOverallStatus(List services) + { + var hasMajorOutage = services.Any(s => s.Status == ServiceHealthStatus.MajorOutage); + var hasMinorIssues = services.Any(s => + s.Status == ServiceHealthStatus.MinorOutage || + s.Status == ServiceHealthStatus.Degraded || + s.Status == ServiceHealthStatus.Maintenance); + + if (hasMajorOutage) + return "Major Issues - Some Systems Down"; + + if (hasMinorIssues) + return "Some Systems Experiencing Issues"; + + return "All Systems Operational"; + } + + #endregion +} diff --git a/TaskGenix.BLL/Services/Implements/TaskAssignmentService.cs b/TaskGenix.BLL/Services/Implements/TaskAssignmentService.cs new file mode 100644 index 0000000..6deda78 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/TaskAssignmentService.cs @@ -0,0 +1,277 @@ +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.Core.Requests.TaskAssignment; +using TaskGenix.Core.Responses.TaskGen; +using TaskGenix.DAL.Data; +using TaskGenix.DAL.Repositories.Interfaces; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements; + +public class TaskAssignmentService : BaseService, ITaskAssignmentService +{ + private readonly ITaskAssignmentRepository _taskAssignmentRepository; + private readonly IGenericRepository _taskRepository; + private readonly IGenericRepository _userRepository; + private readonly IGenericRepository _projectMemberRepository; + + public TaskAssignmentService( + IUnitOfWork unitOfWork, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper) : base(unitOfWork, logger, httpContextAccessor, mapper) + { + _taskAssignmentRepository = unitOfWork.GetRepository() as ITaskAssignmentRepository + ?? throw new InvalidOperationException("TaskAssignmentRepository not properly configured"); + _taskRepository = unitOfWork.GetRepository(); + _userRepository = unitOfWork.GetRepository(); + _projectMemberRepository = unitOfWork.GetRepository(); + } + + public async Task AssignUserToTaskAsync(AssignUserToTaskRequest request) + { + _logger.LogInformation("Assigning user {UserId} to task {TaskId}", request.UserId, request.TaskId); + + // Validate task exists and get project ID + var task = await _taskRepository.FirstOrDefaultAsync( + predicate: t => t.Id == request.TaskId, + include: q => q.Include(t => t.Project)); + + if (task == null) + throw new NotFoundException($"Task with ID {request.TaskId} not found"); + + // Validate user exists + var user = await _userRepository.FirstOrDefaultAsync( + predicate: u => u.Id == request.UserId); + + if (user == null) + throw new NotFoundException($"User with ID {request.UserId} not found"); + + // Validate user is a member of the project + var isProjectMember = await _projectMemberRepository.FirstOrDefaultAsync( + predicate: pm => pm.ProjectId == task.ProjectId && pm.UserId == request.UserId); + + if (isProjectMember == null) + throw new ApiException( + $"User is not a member of the project. Only project members can be assigned to tasks.", + System.Net.HttpStatusCode.Forbidden); + + // Check if already assigned + var existingAssignment = await _taskAssignmentRepository.GetAssignmentByTaskAndUserAsync( + request.TaskId, request.UserId); + + if (existingAssignment != null) + throw new ConflictException("User is already assigned to this task"); + + // Create new assignment + var assignment = new TaskAssignment + { + TaskId = request.TaskId, + UserId = request.UserId, + AssignedAt = DateTime.UtcNow + }; + + await _taskAssignmentRepository.Insert(assignment); + await _unitOfWork.CommitAsync(); + + // Retrieve with details + var createdAssignment = await _taskAssignmentRepository.GetAssignmentWithDetailsAsync(assignment.Id); + + if (createdAssignment == null) + throw new ApiException("Failed to retrieve created assignment", System.Net.HttpStatusCode.InternalServerError); + + _logger.LogInformation("Successfully assigned user {UserId} to task {TaskId}", request.UserId, request.TaskId); + + return _mapper.Map(createdAssignment); + } + + public async Task> BulkAssignUsersToTaskAsync(BulkAssignUsersRequest request) + { + _logger.LogInformation("Bulk assigning {Count} users to task {TaskId}", request.UserIds.Count, request.TaskId); + + // Validate task exists and get project ID + var task = await _taskRepository.FirstOrDefaultAsync( + predicate: t => t.Id == request.TaskId, + include: q => q.Include(t => t.Project)); + + if (task == null) + throw new NotFoundException($"Task with ID {request.TaskId} not found"); + + var responses = new List(); + + foreach (var userId in request.UserIds) + { + try + { + // Check if user exists + var user = await _userRepository.FirstOrDefaultAsync( + predicate: u => u.Id == userId); + + if (user == null) + { + _logger.LogWarning("User {UserId} not found, skipping", userId); + continue; + } + + // Validate user is a member of the project + var isProjectMember = await _projectMemberRepository.FirstOrDefaultAsync( + predicate: pm => pm.ProjectId == task.ProjectId && pm.UserId == userId); + + if (isProjectMember == null) + { + _logger.LogWarning("User {UserId} is not a member of project {ProjectId}, skipping", userId, task.ProjectId); + continue; + } + + // Check if already assigned + var isAssigned = await _taskAssignmentRepository.IsUserAssignedToTaskAsync(request.TaskId, userId); + if (isAssigned) + { + _logger.LogWarning("User {UserId} already assigned to task {TaskId}, skipping", userId, request.TaskId); + continue; + } + + // Create assignment + var assignment = new TaskAssignment + { + TaskId = request.TaskId, + UserId = userId, + AssignedAt = DateTime.UtcNow + }; + + await _taskAssignmentRepository.Insert(assignment); + await _unitOfWork.CommitAsync(); + + var createdAssignment = await _taskAssignmentRepository.GetAssignmentWithDetailsAsync(assignment.Id); + if (createdAssignment != null) + { + responses.Add(_mapper.Map(createdAssignment)); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error assigning user {UserId} to task {TaskId}", userId, request.TaskId); + } + } + + _logger.LogInformation("Successfully assigned {Count} users to task {TaskId}", responses.Count, request.TaskId); + + return responses; + } + + public async Task RemoveUserFromTaskAsync(RemoveUserFromTaskRequest request) + { + _logger.LogInformation("Removing user {UserId} from task {TaskId}", request.UserId, request.TaskId); + + var assignment = await _taskAssignmentRepository.GetAssignmentByTaskAndUserAsync( + request.TaskId, request.UserId); + + if (assignment == null) + throw new NotFoundException("Assignment not found"); + + await _taskAssignmentRepository.Delete(assignment); + var result = await _unitOfWork.CommitAsync() > 0; + + _logger.LogInformation("Successfully removed user {UserId} from task {TaskId}", request.UserId, request.TaskId); + + return result; + } + + public async Task> GetAssignmentsByTaskIdAsync(Guid taskId) + { + _logger.LogInformation("Getting assignments for task {TaskId}", taskId); + + var assignments = await _taskAssignmentRepository.GetAssignmentsByTaskIdAsync(taskId); + + return _mapper.Map>(assignments); + } + + public async Task> GetAssignmentsByUserIdAsync(Guid userId) + { + _logger.LogInformation("Getting assignments for user {UserId}", userId); + + var assignments = await _taskAssignmentRepository.GetAssignmentsByUserIdAsync(userId); + + return _mapper.Map>(assignments); + } + + public async Task> GetMyAssignmentsAsync() + { + var currentUserId = GetCurrentUserId(); + _logger.LogInformation("Getting assignments for current user {UserId}", currentUserId); + + var assignments = await _taskAssignmentRepository.GetAssignmentsByUserIdAsync(currentUserId); + + return _mapper.Map>(assignments); + } + + public async Task GetAssignmentByIdAsync(Guid assignmentId) + { + _logger.LogInformation("Getting assignment {AssignmentId}", assignmentId); + + var assignment = await _taskAssignmentRepository.GetAssignmentWithDetailsAsync(assignmentId); + + if (assignment == null) + throw new NotFoundException($"Assignment with ID {assignmentId} not found"); + + return _mapper.Map(assignment); + } + + public async Task IsUserAssignedToTaskAsync(Guid taskId, Guid userId) + { + return await _taskAssignmentRepository.IsUserAssignedToTaskAsync(taskId, userId); + } + + public async Task RemoveAllAssignmentsForTaskAsync(Guid taskId) + { + _logger.LogInformation("Removing all assignments for task {TaskId}", taskId); + + var result = await _taskAssignmentRepository.RemoveAllAssignmentsForTaskAsync(taskId); + + _logger.LogInformation("Successfully removed all assignments for task {TaskId}", taskId); + + return result; + } + + public async Task> GetAvailableUsersForTaskAsync(Guid taskId) + { + _logger.LogInformation("Getting available users for task {TaskId}", taskId); + + // Get the task with its project information + var task = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + predicate: t => t.Id == taskId, + include: query => query.Include(t => t.Project)); + + if (task == null) + throw new NotFoundException($"Task with ID {taskId} not found"); + + // Get all project members for this task's project + var projectMembers = await _projectMemberRepository + .GetListAsync( + predicate: pm => pm.ProjectId == task.ProjectId, + include: query => query.Include(pm => pm.User)); + + // Map to UserResponse + var users = projectMembers + .Select(pm => pm.User) + .Where(u => u != null) + .Select(u => new Core.Responses.User.UserResponse + { + Id = u!.Id, + Username = u.Username, + Email = u.Email, + ProfilePicture = u.AvatarUrl + }) + .ToList(); + + _logger.LogInformation("Found {Count} available users for task {TaskId}", users.Count, taskId); + + return users; + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/TaskGenService.cs b/TaskGenix.BLL/Services/Implements/TaskGenService.cs index e044ec7..830265a 100644 --- a/TaskGenix.BLL/Services/Implements/TaskGenService.cs +++ b/TaskGenix.BLL/Services/Implements/TaskGenService.cs @@ -1,13 +1,8 @@ -using ClaimRequest.BLL.Services; using Mapster; using MapsterMapper; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using TaskGenix.BLL.Services.Interfaces; using TaskGenix.Core.Domains; using TaskGenix.Core.Requests.TaskGen; @@ -15,115 +10,354 @@ using TaskGenix.DAL.Data; using TaskGenix.Repositories.Interfaces; -namespace TaskGenix.BLL.Services.Implements -{ - public class TaskGenService : BaseService, ITaskGenService - { +namespace TaskGenix.BLL.Services.Implements; - public TaskGenService(IUnitOfWork unitOfWork, +public class TaskGenService : BaseService, ITaskGenService +{ + public TaskGenService(IUnitOfWork unitOfWork, ILogger logger, IHttpContextAccessor httpContextAccessor, IMapper mapper) : base(unitOfWork, logger, httpContextAccessor, mapper) - { - } + { + } - public async Task> GetAllTasks() - { - return await _unitOfWork.GetRepository().GetListAsync(); - } + #region Include Strategies - public async Task GetTaskById(Guid taskId) - { - return await _unitOfWork.GetRepository().FirstOrDefaultAsync( - predicate: t => t.Id == taskId, - include: query => query - .Include(t => t.Assignments) - .ThenInclude(ta => ta.AssignedUser) - .AsNoTracking()) - ?? throw new KeyNotFoundException($"Task with ID {taskId} not found."); - } - - public async Task> GetTasksByProjectId(Guid projectId) + private Func, IQueryable> ApplyTaskListIncludes() + { + return query => query + .Include(t => t.ResourceLinks) // Resources needed for mobile + .AsNoTracking(); + } + + /// + /// Full includes for detail views - includes all nested data + /// Use when full task data with assignments is needed + /// + private Func, IQueryable> ApplyTaskDetailIncludes() + { + return query => query + .Include(t => t.Assignments) + .ThenInclude(ta => ta.AssignedUser) + .Include(t => t.ResourceLinks) + .AsNoTracking(); + } + + #endregion + + #region Public Methods + + public async Task> GetAllTasks() + { + // Use lightweight includes - list view doesn't need assignment details + var tasks = await _unitOfWork.GetRepository().GetListAsync( + include: ApplyTaskListIncludes() + ); + + return tasks.Adapt>(); + } + + public async Task> GetTasksByProjectId(Guid projectId) + { + // Use lightweight includes - optimized for project task list + var tasks = await _unitOfWork.GetRepository().GetListAsync( + predicate: t => t.ProjectId == projectId, + include: ApplyTaskListIncludes() + ); + + return tasks.Adapt>(); + } + + public async Task> GetTasksByUserId(Guid userId) + { + // Use lightweight includes - optimized for user task list + var tasks = await _unitOfWork.GetRepository().GetListAsync( + predicate: t => t.Assignments.Any(ta => ta.UserId == userId), + include: ApplyTaskListIncludes() + ); + + return tasks.Adapt>(); + } + + public async Task GetTaskById(Guid taskId) + { + var task = await _unitOfWork.GetRepository().FirstOrDefaultAsync( + predicate: t => t.Id == taskId, + include: ApplyTaskDetailIncludes() + ) ?? throw new KeyNotFoundException($"Task with ID {taskId} not found."); + + return task.Adapt(); + } + + #endregion + + // Task-Project assignment methods + public async Task AssignTaskToProject(Guid taskId, Guid projectId) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => { - return await _unitOfWork.GetRepository().GetListAsync(predicate: t => t.ProjectId == projectId); - } + var task = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: t => t.Id == taskId); + if (task == null) + throw new KeyNotFoundException($"Task with ID {taskId} not found"); + + var project = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: p => p.Id == projectId); + if (project == null) + throw new KeyNotFoundException($"Project with ID {projectId} not found"); + + task.ProjectId = projectId; + task.UpdatedAt = DateTime.UtcNow; - public async Task> GetTasksByUserId(Guid userId) + await _unitOfWork.GetRepository().Update(task); + await _unitOfWork.CommitAsync(); + + return await GetTaskById(taskId); + }); + } + + public async Task UnassignTaskFromProject(Guid taskId) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => { - var tasks = await _unitOfWork.GetRepository().GetListAsync( - predicate: t => t.Assignments.Any(ta => ta.UserId == userId) - ); - return tasks; - } + var task = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: t => t.Id == taskId); + if (task == null) + throw new KeyNotFoundException($"Task with ID {taskId} not found"); + + task.ProjectId = Guid.Empty; + task.UpdatedAt = DateTime.UtcNow; - public async Task CreateTask(TaskGen task) + await _unitOfWork.GetRepository().Update(task); + await _unitOfWork.CommitAsync(); + + return await GetTaskById(taskId); + }); + } + + public async Task CreateTask(CreateTaskRequest request) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => { - return await _unitOfWork.ExecuteInTransactionAsync(async () => + var project = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: p => p.Id == request.ProjectId); + if (project == null) throw new KeyNotFoundException($"Project with ID {request.ProjectId} not found"); + + var task = _mapper.Map(request); + + await _unitOfWork.GetRepository().Insert(task); + + // Add task assignments if specified + if (request.AssignedUserIds != null && request.AssignedUserIds.Any()) { - // chưa validate business logic cho task - await _unitOfWork.GetRepository().Insert(task); - return task; - }); - } + // Remove duplicates and validate users in batch + var distinctUserIds = request.AssignedUserIds.Distinct().ToList(); + + var existingUsers = await _unitOfWork.GetRepository() + .GetListAsync(predicate: u => distinctUserIds.Contains(u.Id)); + + var existingUserIds = existingUsers.Select(u => u.Id).ToHashSet(); + + var assignments = distinctUserIds + .Where(userId => existingUserIds.Contains(userId)) + .Select(userId => new TaskAssignment + { + TaskId = task.Id, + UserId = userId, + AssignedAt = DateTime.UtcNow + }) + .ToList(); + + if (assignments.Any()) + await _unitOfWork.GetRepository().InsertRange(assignments); + } + + await _unitOfWork.CommitAsync(); + return task; + }); + } - public async Task> CreateTaskBulk(IEnumerable tasks) + public async Task> CreateTaskBulk(IEnumerable requests) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => { - return await _unitOfWork.ExecuteInTransactionAsync(async () => + // get distinct project id from request + var requestsList = requests.ToList(); + var projectIds = requestsList.Select(r => r.ProjectId).Distinct().ToList(); + var allProjects = await _unitOfWork.GetRepository() + .GetListAsync(predicate: p => projectIds.Contains(p.Id)); + + // create dictionary for fast lookup + var projectDict = allProjects.ToDictionary(p => p.Id); + + // get distinct user ids from all requests + var userIds = requestsList + .SelectMany(r => r.AssignedUserIds ?? Enumerable.Empty()) + .Distinct() + .ToList(); + + // get all User objects needed in one query + var allUsers = userIds.Any() + ? await _unitOfWork.GetRepository().GetListAsync(predicate: u => userIds.Contains(u.Id)) + : new List(); + + // create dictionary for fast lookup + var userDict = allUsers.ToDictionary(u => u.Id); + + // using single UtcNow call + var now = DateTime.UtcNow; + + var tasks = new List(); + var allAssignments = new List(); + var allResourceLinks = new List(); + + // Process each request + foreach (var request in requestsList) { - // chưa validate business logic cho tasks - await _unitOfWork.GetRepository().InsertRange(tasks); - return tasks; - }); - } + // Validate project existence from dictionary + if (!projectDict.TryGetValue(request.ProjectId, out var project)) + throw new KeyNotFoundException($"Project with ID {request.ProjectId} not found"); - public Task> UpdateTaskBulk(IEnumerable tasks) - { - throw new NotImplementedException(); - } + var task = new TaskGen + { + Title = request.Title, + Description = request.Description ?? "", + DueDate = request.DueDate, + ProjectId = request.ProjectId, + Status = request.Status, + Priority = request.Priority, + CreatedAt = now, + UpdatedAt = now + }; - public Task DeleteTask(Guid taskId) - { - throw new NotImplementedException(); - } + tasks.Add(task); - public Task UpdateTask(TaskGen task) - { - throw new NotImplementedException(); - } + // Prepare assignments for this task - lookup user from dictionary + if (request.AssignedUserIds != null && request.AssignedUserIds.Any()) + { + var distinctUserIds = request.AssignedUserIds.Distinct(); + foreach (var userId in distinctUserIds) + { + // Validate user existence from dictionary + if (userDict.TryGetValue(userId, out _)) + allAssignments.Add(new TaskAssignment + { + TaskId = task.Id, + UserId = userId, + AssignedAt = now + }); + } + } - // DTO implementations using Mapster for projection - public async Task> GetAllTasksDto() - { - return await _unitOfWork.GetRepository().GetListAsync( - selector: task => task.Adapt() - ); - } + // Prepare resource links for this task + if (request.ResourceLinks != null && request.ResourceLinks.Any()) + { + foreach (var link in request.ResourceLinks) + { + allResourceLinks.Add(new ResourceLink + { + Id = Guid.NewGuid(), + Title = link.Title, + Url = link.Url, + Description = link.Description, + Type = link.Type, + TaskGenId = task.Id + }); + } + } + } - public async Task> GetTasksByProjectIdDto(Guid projectId) - { - return await _unitOfWork.GetRepository().GetListAsync( - selector: task => task.Adapt(), - predicate: t => t.ProjectId == projectId - ); - } + // Bulk insert tasks and assignments + await _unitOfWork.GetRepository().InsertRange(tasks); - public async Task> GetTasksByUserIdDto(Guid userId) - { - return await _unitOfWork.GetRepository().GetListAsync( - selector: task => task.Adapt(), - predicate: t => t.Assignments.Any(ta => ta.UserId == userId) - ); - } + if (allAssignments.Any()) + await _unitOfWork.GetRepository().InsertRange(allAssignments); - public async Task GetTaskByIdDto(Guid taskId) + if (allResourceLinks.Any()) + await _unitOfWork.GetRepository().InsertRange(allResourceLinks); + + await _unitOfWork.CommitAsync(); + return tasks; + }); + } + + public async Task UpdateTask(Guid taskId, UpdateTaskRequest request) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => { - return await _unitOfWork.GetRepository().FirstOrDefaultAsync( - selector: task => task.Adapt(), + var task = await _unitOfWork.GetRepository().FirstOrDefaultAsync( predicate: t => t.Id == taskId, - include: query => query - .Include(t => t.Assignments) - .ThenInclude(ta => ta.AssignedUser) - .AsNoTracking() - ); - } + include: query => query.Include(t => t.Assignments)); + + if (task == null) throw new KeyNotFoundException($"Task with ID {taskId} not found"); + + // Update task properties if provided + if (!string.IsNullOrEmpty(request.Title)) + task.Title = request.Title; + + if (request.Description != null) + task.Description = request.Description; + + if (request.DueDate.HasValue) + task.DueDate = request.DueDate; + + task.Status = request.Status; + task.Priority = request.Priority; + task.UpdatedAt = DateTime.UtcNow; + + // Update assignments if specified + if (request.AssignedUserIds != null) + { + // Remove duplicates from request + var distinctUserIds = request.AssignedUserIds.Distinct().ToList(); + + // Batch validate users + var existingUsers = await _unitOfWork.GetRepository() + .GetListAsync(predicate: u => distinctUserIds.Contains(u.Id)); + var existingUserIds = existingUsers.Select(u => u.Id).ToHashSet(); + + // Remove existing assignments in batch + var existingAssignments = task.Assignments.ToList(); + if (existingAssignments.Any()) + await _unitOfWork.GetRepository().DeleteRange(existingAssignments); + + // Add new assignments + var newAssignments = distinctUserIds + .Where(userId => existingUserIds.Contains(userId)) + .Select(userId => new TaskAssignment + { + TaskId = taskId, + UserId = userId, + AssignedAt = DateTime.UtcNow + }) + .ToList(); + + if (newAssignments.Any()) + await _unitOfWork.GetRepository().InsertRange(newAssignments); + } + + await _unitOfWork.GetRepository().Update(task); + await _unitOfWork.CommitAsync(); + + return task; + }); + } + + public async Task DeleteTask(Guid taskId) + { + return await _unitOfWork.ExecuteInTransactionAsync(async () => + { + var task = await _unitOfWork.GetRepository().FirstOrDefaultAsync(predicate: t => t.Id == taskId); + if (task == null) return false; + + var assignments = await _unitOfWork.GetRepository().GetListAsync( + predicate: ta => ta.TaskId == taskId); + + if (assignments.Any()) await _unitOfWork.GetRepository().DeleteRange(assignments); + + await _unitOfWork.GetRepository().Delete(task); + await _unitOfWork.CommitAsync(); + + return true; + }); } } \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/UserDeviceTokenService.cs b/TaskGenix.BLL/Services/Implements/UserDeviceTokenService.cs new file mode 100644 index 0000000..e19be60 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/UserDeviceTokenService.cs @@ -0,0 +1,358 @@ +using TaskGenix.BLL.Services; +using Mapster; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.UserDeviceToken; +using TaskGenix.Core.Responses.UserDeviceToken; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements +{ + public class UserDeviceTokenService : BaseService, IUserDeviceTokenService + { + public UserDeviceTokenService( + IUnitOfWork unitOfWork, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + IMapper mapper) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + } + + public async Task> GetAllTokensAsync(int page = 1, int size = 20) + { + try + { + var pagedTokens = await _unitOfWork.GetRepository() + .GetPagingListAsync( + selector: token => token.Adapt(), + orderBy: query => query.OrderByDescending(t => t.CreatedAt), + page: page, + size: size); + + return pagedTokens; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get all device tokens"); + throw; + } + } + + public async Task GetTokenByIdAsync(Guid id) + { + try + { + var token = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + selector: t => t.Adapt(), + predicate: t => t.Id == id); + + if (token == null) + { + throw new KeyNotFoundException($"Device token with ID {id} not found"); + } + + return token; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get device token: {TokenId}", id); + throw; + } + } + + public async Task CreateOrUpdateTokenAsyncV2(CreateUserDeviceTokenRequestV2 request) + { + const int maxRetryAttempts = 3; + const int delayBetweenRetriesMs = 1000; + + for (int attempt = 1; attempt <= maxRetryAttempts; attempt++) + { + try + { + // Get current user email from JWT token using BaseService method + var userEmail = GetCurrentUserEmail(); + + // Validate email is not empty (timing issue check) + if (string.IsNullOrWhiteSpace(userEmail)) + { + if (attempt < maxRetryAttempts) + { + _logger.LogWarning("User email not available from JWT token, attempt {Attempt}/{MaxAttempts}. Retrying...", + attempt, maxRetryAttempts); + await Task.Delay(delayBetweenRetriesMs); + continue; + } + throw new UnauthorizedAccessException("User email not found in JWT token. Please ensure you are properly authenticated."); + } + + /* + If the same device (DeviceId) with the same platform tries to register again, + it will update the existing record with the new user's email and FCM token + This handles cases where a device might be used by different users over time*/ + var existingToken = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: t => t.DeviceId == request.DeviceId && t.Platform == request.Platform); + + if (existingToken != null) + { + // Update existing token with current user's email and new FCM token + existingToken.Email = userEmail; // Update email to current user + existingToken.FCMToken = request.FCMToken; + existingToken.DeviceName = request.DeviceName ?? existingToken.DeviceName; + existingToken.Platform = request.Platform; + existingToken.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(existingToken); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Updated existing device token for user: {Email}, device: {DeviceId}, platform: {Platform}, attempt: {Attempt}", + userEmail, request.DeviceId, request.Platform, attempt); + + return existingToken.Adapt(); + } + else + { + // Create new token + var newToken = new UserDeviceToken + { + Email = userEmail, + DeviceId = request.DeviceId, + FCMToken = request.FCMToken, + DeviceName = request.DeviceName, + Platform = request.Platform, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().Insert(newToken); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Created new device token for user: {Email}, device: {DeviceId}, platform: {Platform}, attempt: {Attempt}", + userEmail, request.DeviceId, request.Platform, attempt); + + return newToken.Adapt(); + } + } + catch (UnauthorizedAccessException) + { + // Don't retry authorization issues, rethrow immediately + throw; + } + catch (ArgumentException) + { + // Don't retry validation issues, rethrow immediately + throw; + } + catch (Exception ex) when (attempt < maxRetryAttempts) + { + _logger.LogWarning(ex, "Failed to create/update device token, attempt {Attempt}/{MaxAttempts}. Retrying...", + attempt, maxRetryAttempts); + await Task.Delay(delayBetweenRetriesMs * attempt); // Exponential backoff + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create/update device token after {MaxAttempts} attempts", maxRetryAttempts); + throw; + } + } + + // This should never be reached due to the exception handling above + throw new InvalidOperationException("Unexpected end of retry loop"); + } + + public async Task UpdateTokenAsync(Guid id, UpdateUserDeviceTokenRequest request) + { + try + { + var token = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: t => t.Id == id); + + if (token == null) + { + throw new KeyNotFoundException($"Device token with ID {id} not found"); + } + + // Update only provided fields + if (!string.IsNullOrEmpty(request.FCMToken)) + token.FCMToken = request.FCMToken; + + if (!string.IsNullOrEmpty(request.DeviceName)) + token.DeviceName = request.DeviceName; + + if (!string.IsNullOrEmpty(request.Platform)) + token.Platform = request.Platform; + + token.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(token); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Updated device token: {TokenId}", id); + + return token.Adapt(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update device token: {TokenId}", id); + throw; + } + } + + public async Task> GetFcmTokensByUserEmailAsync(string email) + { + + try + { + var fcmTokens = await _unitOfWork.GetRepository() + .GetListAsync( + selector: t => t.FCMToken, + predicate: t => t.Email == email); + + return fcmTokens; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get FCM tokens for user: {Email}", email); + throw; + } + + } + + public async Task DeleteTokenAsync(Guid id) + { + try + { + var token = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: t => t.Id == id); + + if (token == null) + { + throw new KeyNotFoundException($"Device token with ID {id} not found"); + } + + await _unitOfWork.GetRepository().Delete(token); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Deleted device token: {TokenId}", id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete device token: {TokenId}", id); + throw; + } + } + + /// + /// Creates or updates FCM token with email parameter (for cases where JWT is not available) + /// This method provides an alternative registration path that doesn't rely on JWT timing + /// + /// User email + /// Token registration request + /// Token response + public async Task CreateOrUpdateTokenWithEmailAsync(string email, CreateUserDeviceTokenRequestV2 request) + { + try + { + // Validate email + if (string.IsNullOrWhiteSpace(email) || !email.Contains('@')) + { + throw new ArgumentException("Valid email address is required"); + } + + // Check for existing token with same device and platform + var existingToken = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(predicate: t => t.DeviceId == request.DeviceId && t.Platform == request.Platform); + + if (existingToken != null) + { + // Update existing token + existingToken.Email = email; + existingToken.FCMToken = request.FCMToken; + existingToken.DeviceName = request.DeviceName ?? existingToken.DeviceName; + existingToken.Platform = request.Platform; + existingToken.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(existingToken); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Updated device token via email method for user: {Email}, device: {DeviceId}, platform: {Platform}", + email, request.DeviceId, request.Platform); + + return existingToken.Adapt(); + } + else + { + // Create new token + var newToken = new UserDeviceToken + { + Email = email, + DeviceId = request.DeviceId, + FCMToken = request.FCMToken, + DeviceName = request.DeviceName, + Platform = request.Platform, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + await _unitOfWork.GetRepository().Insert(newToken); + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Created device token via email method for user: {Email}, device: {DeviceId}, platform: {Platform}", + email, request.DeviceId, request.Platform); + + return newToken.Adapt(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create/update device token via email method for user: {Email}", email); + throw; + } + } + + /// + /// Cleans up expired or invalid FCM tokens + /// + /// Remove tokens older than this number of days + /// Number of tokens removed + public async Task CleanupExpiredTokensAsync(int olderThanDays = 90) + { + try + { + var cutoffDate = DateTime.UtcNow.AddDays(-olderThanDays); + + var expiredTokens = await _unitOfWork.GetRepository() + .GetListAsync(predicate: t => t.UpdatedAt < cutoffDate); + + if (expiredTokens.Any()) + { + foreach (var token in expiredTokens) + { + await _unitOfWork.GetRepository().Delete(token); + } + + await _unitOfWork.CommitAsync(); + + _logger.LogInformation("Cleaned up {Count} expired FCM tokens older than {Days} days", + expiredTokens.Count(), olderThanDays); + + return expiredTokens.Count(); + } + + return 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to cleanup expired FCM tokens"); + throw; + } + } + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/UserPlanFeatureService.cs b/TaskGenix.BLL/Services/Implements/UserPlanFeatureService.cs new file mode 100644 index 0000000..4da74dd --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/UserPlanFeatureService.cs @@ -0,0 +1,166 @@ +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System.Linq.Expressions; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.UserPlanFeature; +using TaskGenix.Core.Responses.UserPlanFeature; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements +{ + public class UserPlanFeatureService : BaseService, IUserPlanFeatureService + { + public UserPlanFeatureService(IUnitOfWork unitOfWork, ILogger logger, IHttpContextAccessor httpContextAccessor, IMapper mapper) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + } + + public async Task CreateUserPlanFeature(CreateUserPlanFeatureRequest request) + { + // disambiguate overloads by using the generic selector overload and passing selector + predicate + Expression> planFeaturePredicate = pf => pf.Id == request.PlanFeatureId; + var planFeature = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(pf => pf, planFeaturePredicate, null, q => q.Include(pf => pf.Feature)); + + if (planFeature == null) + throw new KeyNotFoundException($"PlanFeature with ID {request.PlanFeatureId} not found."); + + var userPlan = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(up => up, up => up.Id == request.UserPlanId); + + if (userPlan == null) + throw new KeyNotFoundException($"UserPlan with ID {request.UserPlanId} not found."); + + var existing = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(upf => upf, upf => upf.UserPlanId == request.UserPlanId && upf.PlanFeatureId == request.PlanFeatureId); + + if (existing != null) + throw new InvalidOperationException("This PlanFeature already exists for the specified UserPlan."); + + var userPlanFeature = new UserPlanFeature + { + UserPlanId = request.UserPlanId, + PlanFeatureId = request.PlanFeatureId, + QuotaTotal = request.QuotaTotal, + QuotaUsed = request.QuotaUsed, + IsUnlimited = request.IsUnlimited, + FeatureKey = planFeature.Feature?.Key + }; + userPlanFeature.RecalculateQuotaRemaining(); + + await _unitOfWork.GetRepository().Insert(userPlanFeature); + await _unitOfWork.CommitAsync(); + + // Reload with includes for mapping using the generic selector overload + var reloaded = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(upf => upf, upf => upf.Id == userPlanFeature.Id, null, q => q.Include(upf => upf.PlanFeature).ThenInclude(pf => pf.Feature)); + + var response = reloaded != null ? _mapper.Map(reloaded) : _mapper.Map(userPlanFeature); + + return response; + } + + public async Task DeleteUserPlanFeature(Guid id) + { + var repo = _unitOfWork.GetRepository(); + + // disambiguate by using generic selector overload + var existing = await repo.FirstOrDefaultAsync(upf => upf, upf => upf.Id == id); + if (existing == null) + return false; + + await repo.Delete(existing); + await _unitOfWork.CommitAsync(); + return true; + } + + public async Task> GetUserPlanFeaturesByUserPlanId(Guid userPlanId) + { + var features = await _unitOfWork.GetRepository() + .GetListAsync( + selector: upf => _mapper.Map(upf), + predicate: upf => upf.UserPlanId == userPlanId, + include: q => q.Include(upf => upf.PlanFeature) + .ThenInclude(pf => pf.Feature)); + + return features; + } + + public async Task UpdateUserPlanFeature(UpdateUserPlanFeatureRequest request) + { + // Build expression predicate using FeatureKey + Expression> predicateExpr = upf => upf.UserPlanId == request.UserPlanId && upf.FeatureKey == request.FeatureKey; + + // use generic selector overload to avoid ambiguity + var existing = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(upf => upf, predicateExpr, null, q => q.Include(upf => upf.PlanFeature).ThenInclude(pf => pf.Feature)); + + if (existing == null) + { + return new UpdateUserPlanFeatureResponse + { + Updated = false, + Message = "UserPlanFeature not found." + }; + } + + // Determine feature type (if available) + var featureType = existing.PlanFeature?.Feature?.FeatureType; + + // Apply changes carefully depending on feature type + if (featureType == Core.Domains.FeatureType.Boolean) + { + // Boolean features: No updates applied as QuotaUsed and IsUnlimited are not relevant + // Do nothing + } + else + { + var absoluteFeatures = new[] { "projects_limit", "tasks_per_project", "storage_limit", "ai_generations" }; + + if (request.QuotaUsed.HasValue) + { + if (request.QuotaUsed.Value < 0) + return new UpdateUserPlanFeatureResponse { Updated = false, Message = "QuotaUsed cannot be negative." }; + + if (absoluteFeatures.Contains(existing.FeatureKey)) + { + // Absolute set, check against remaining quota if not unlimited + if (!existing.IsUnlimited && request.QuotaUsed.Value > existing.QuotaRemaining) + return new UpdateUserPlanFeatureResponse { Updated = false, Message = "QuotaUsed cannot exceed remaining quota." }; + existing.QuotaUsed = request.QuotaUsed.Value; + } + else + { + // Increment for other features + existing.QuotaUsed += request.QuotaUsed.Value; + } + + // ensure QuotaUsed does not exceed QuotaTotal (clamp) + if (!existing.IsUnlimited && existing.QuotaUsed > existing.QuotaTotal) + existing.QuotaUsed = existing.QuotaTotal.Value; + } + } + + // Recalculate remaining quota + existing.RecalculateQuotaRemaining(); + await _unitOfWork.GetRepository().Update(existing); + await _unitOfWork.CommitAsync(); + + // Reload with includes for mapping (generic selector overload) + var reloaded = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync(upf => upf, upf => upf.Id == existing.Id, null, q => q.Include(upf => upf.PlanFeature).ThenInclude(pf => pf.Feature)); + + var data = reloaded != null ? _mapper.Map(reloaded) : null; + + return new UpdateUserPlanFeatureResponse + { + Updated = true, + Data = data + }; + } + } +} diff --git a/TaskGenix.BLL/Services/Implements/UserPlanService.cs b/TaskGenix.BLL/Services/Implements/UserPlanService.cs new file mode 100644 index 0000000..178ba48 --- /dev/null +++ b/TaskGenix.BLL/Services/Implements/UserPlanService.cs @@ -0,0 +1,96 @@ +using TaskGenix.BLL.Services; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.UserPlan; +using TaskGenix.Core.Responses.UserPlan; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Implements; +using TaskGenix.Repositories.Interfaces; + +namespace TaskGenix.BLL.Services.Implements +{ + public class UserPlanService : BaseService, IUserPlanService + { + public UserPlanService(IUnitOfWork unitOfWork, ILogger logger, IHttpContextAccessor httpContextAccessor, IMapper mapper) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + } + + public async Task> GetAllUserPlans() + { + var userPlans = await _unitOfWork.GetRepository() + .GetListAsync( + selector: userPlan => _mapper.Map(userPlan), + include: q => q.Include(up => up.Features) + .ThenInclude(upf => upf.PlanFeature)); + return userPlans; + } + + public async Task GetUserPlanById(Guid id) + { + var userPlan = await _unitOfWork.GetRepository() + .FirstOrDefaultAsync( + selector: userPlan => _mapper.Map(userPlan), + include: q => q.Include(up => up.Features) + .ThenInclude(upf => upf.PlanFeature)) + ?? throw new KeyNotFoundException($"UserPlan with ID {id} not found."); + return userPlan; + } + + public async Task CreateUserPlan(UserPlanRequest userPlanRequest) + { + var userPlan = _mapper.Map(userPlanRequest); + userPlan.StartDate = DateTime.SpecifyKind(userPlan.StartDate, DateTimeKind.Utc); + userPlan.EndDate = DateTime.SpecifyKind(userPlan.EndDate, DateTimeKind.Utc); + await _unitOfWork.GetRepository().Insert(userPlan); + await _unitOfWork.CommitAsync(); + return _mapper.Map(userPlan); + } + + public async Task UpdateUserPlan(Guid id, UpdateUserPlanRequest updateUserPlanRequest) + { + var userPlanRepository = _unitOfWork.GetRepository(); + var existingUserPlan = await userPlanRepository.FirstOrDefaultAsync( + predicate: up => up.Id == id, + include: q => q.Include(up => up.Features) + .ThenInclude(upf => upf.PlanFeature)); + if (existingUserPlan == null) + { + return null; + } + + _mapper.Map(updateUserPlanRequest, existingUserPlan); + await userPlanRepository.Update(existingUserPlan); + await _unitOfWork.CommitAsync(); + + return _mapper.Map(existingUserPlan); + } + + public async Task DeleteUserPlan(Guid id) + { + var userPlanRepository = _unitOfWork.GetRepository(); + var existingUserPlan = await userPlanRepository.FirstOrDefaultAsync( + predicate: up => up.Id == id, + include: q => q.Include(up => up.Features) + .ThenInclude(upf => upf.PlanFeature)); + + if (existingUserPlan == null) + { + return false; + } + + await userPlanRepository.Delete(existingUserPlan); + await _unitOfWork.CommitAsync(); + + return true; + } + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Implements/UserService.cs b/TaskGenix.BLL/Services/Implements/UserService.cs index 338bb15..ff65d58 100644 --- a/TaskGenix.BLL/Services/Implements/UserService.cs +++ b/TaskGenix.BLL/Services/Implements/UserService.cs @@ -2,46 +2,266 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using ClaimRequest.BLL.Services; +using TaskGenix.BLL.Services; using MapsterMapper; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; using TaskGenix.Core.Responses.User; using TaskGenix.DAL.Data; using TaskGenix.Repositories.Interfaces; +using BCrypt.Net; +using Mapster; +using TaskGenix.Core.Exceptions; +using TaskGenix.Core.Meta; -namespace TaskGenix.BLL.Services.Implements +namespace TaskGenix.BLL.Services.Implements; + +public class UserService : BaseService, IUserService { - public class UserService : BaseService, IUserService + public UserService(IUnitOfWork unitOfWork, ILogger logger, + IHttpContextAccessor httpContextAccessor, IMapper mapper) + : base(unitOfWork, logger, httpContextAccessor, mapper) + { + } + + public async Task> GetAllUsers() + { + _logger.LogInformation("Getting all users"); + + var users = await _unitOfWork.GetRepository() + .GetListAsync(user => user.Adapt()); + + _logger.LogInformation($"Retrieved {users.Count()} users"); + return users; + } + + public async Task> GetUsersPaged(int page, int pageSize) + { + _logger.LogInformation("Getting paged users, page: {Page}, size: {PageSize}", page, pageSize); + + var result = await _unitOfWork.GetRepository() + .GetPagingListAsync( + user => user.Adapt(), // project User → UserResponse + null, // no filter + q => q.OrderBy(u => u.CreatedAt), // optional ordering + page: page, + size: pageSize + ); + + return result; + } + + public async Task GetUserById(Guid id) + { + _logger.LogInformation($"Getting user by id: {id}"); + + var user = await _unitOfWork.GetRepository().FirstOrDefaultAsync(predicate: u => u.Id == id); + if (user == null) throw new NotFoundException("User not found"); + + var userResponse = _mapper.Map(user); + _logger.LogInformation($"Successfully retrieved user: {user.Username}"); + + return userResponse; + } + + public async Task GetUserByAuth0Id(string id) + { + _logger.LogInformation($"Getting user by Auth0id: {id}"); + + var user = await _unitOfWork.GetRepository().FirstOrDefaultAsync(predicate: u => u.Auth0Id == id); + if (user == null) throw new NotFoundException("User not found"); + + var userResponse = _mapper.Map(user); + _logger.LogInformation($"Successfully retrieved user: {user.Username}"); + + return userResponse; + } + + public async Task CreateUser(UserRequest userRequest) + { + _logger.LogInformation($"Creating new user with username: {userRequest.Username}"); + + // Check if user already exists with the same email or username + var existingUsers = await _unitOfWork.GetRepository().GetListAsync(); + var existingUser = existingUsers.FirstOrDefault(u => + u.Email.ToLower() == userRequest.Email?.ToLower() || + u.Username.ToLower() == userRequest.Username?.ToLower()); + + if (existingUser != null) + { + _logger.LogWarning( + $"User with email {userRequest.Email} or username {userRequest.Username} already exists"); + throw new InvalidOperationException("User with this email or username already exists"); + } + + // Create new user entity + var user = new User + { + Id = Guid.NewGuid(), + Username = userRequest.Username ?? throw new ArgumentException("Username is required"), + Email = userRequest.Email ?? throw new ArgumentException("Email is required"), + PasswordHash = + BCrypt.Net.BCrypt.HashPassword(userRequest.Password ?? + throw new ArgumentException("Password is required")), + Role = UserRole.User, // Default role + IsActive = true, + EmailVerified = false, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + user.AvatarUrl = $"https://api.dicebear.com/9.x/adventurer/png?seed={user.Username}"; + + await _unitOfWork.GetRepository().Insert(user); + await _unitOfWork.CommitAsync(); + + var userResponse = _mapper.Map(user); + _logger.LogInformation($"Successfully created user: {user.Username} with id: {user.Id}"); + + return userResponse; + } + + public async Task UpdateUser(Guid id, UpdateUserRequest userRequest) { - public UserService(IUnitOfWork unitOfWork, ILogger logger, IHttpContextAccessor httpContextAccessor, IMapper mapper) : base(unitOfWork, logger, httpContextAccessor, mapper) + _logger.LogInformation($"Updating entire user with id: {id}"); + + var user = await _unitOfWork.GetRepository().FirstOrDefaultAsync(predicate: u => u.Id == id); + if (user == null) throw new NotFoundException("User not found"); + + // Validate required fields for PUT operation + if (string.IsNullOrEmpty(userRequest.Username)) + throw new ArgumentException("Username is required for full update"); + if (string.IsNullOrEmpty(userRequest.Email)) + throw new ArgumentException("Email is required for full update"); + if (string.IsNullOrEmpty(userRequest.Password)) + throw new ArgumentException("Password is required for full update"); + + // Check if email or username conflicts with existing users + await ValidateUniqueEmailAndUsername(userRequest.Email, userRequest.Username, id); + + // Update all user properties (PUT - full replacement) + user.Username = userRequest.Username; + user.Email = userRequest.Email; + user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(userRequest.Password); + user.EmailVerified = false; // Reset email verification when email changes + user.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(user); + await _unitOfWork.CommitAsync(); + + var userResponse = _mapper.Map(user); + _logger.LogInformation($"Successfully updated entire user: {user.Username} with id: {id}"); + + return userResponse; + } + + public async Task UpdatePartialUser(Guid id, UpdatePartialUserRequest userRequest) + { + _logger.LogInformation($"Partially updating user with id: {id}"); + + var user = await _unitOfWork.GetRepository().FirstOrDefaultAsync(predicate: u => u.Id == id); + if (user == null) throw new NotFoundException("User not found"); + + // Check if email or username is being changed and if it conflicts with existing users + if (!string.IsNullOrEmpty(userRequest.Email) && userRequest.Email.ToLower() != user.Email.ToLower()) + await ValidateUniqueEmailAndUsername(userRequest.Email, null, id); + + if (!string.IsNullOrEmpty(userRequest.Username) && userRequest.Username.ToLower() != user.Username.ToLower()) + await ValidateUniqueEmailAndUsername(null, userRequest.Username, id); + + // Update only provided properties (PATCH - partial update) + var hasChanges = false; + + if (!string.IsNullOrEmpty(userRequest.Username)) { + user.Username = userRequest.Username; + hasChanges = true; } - public Task CreateUser(UserResponse user) + if (!string.IsNullOrEmpty(userRequest.Email)) { - throw new NotImplementedException(); + user.Email = userRequest.Email; + user.EmailVerified = false; // Reset email verification if email changed + hasChanges = true; } - public Task DeleteUser(Guid id) + if (!string.IsNullOrEmpty(userRequest.Password)) { - throw new NotImplementedException(); + user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(userRequest.Password); + hasChanges = true; } - public Task> GetAllUsers() + if (!hasChanges) { - throw new NotImplementedException(); + _logger.LogInformation($"No changes provided for user with id: {id}"); + return _mapper.Map(user); } - public Task GetUserById(Guid id) + user.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(user); + await _unitOfWork.CommitAsync(); + + var userResponse = _mapper.Map(user); + _logger.LogInformation($"Successfully partially updated user: {user.Username} with id: {id}"); + + return userResponse; + } + + public async Task DeleteUser(Guid id) + { + _logger.LogInformation($"Deleting user with id: {id}"); + + var user = await _unitOfWork.GetRepository().FirstOrDefaultAsync(predicate: u => u.Id == id); + if (user == null) + { + _logger.LogWarning($"User with id {id} not found for deletion"); + throw new NotFoundException("User not found"); + } + + // Soft delete by setting IsActive to false (recommended for audit trails) + user.IsActive = false; + user.UpdatedAt = DateTime.UtcNow; + + await _unitOfWork.GetRepository().Update(user); + + // Uncomment the line below if you want hard delete instead + // _unitOfWork.GetRepository().Delete(user); + + await _unitOfWork.CommitAsync(); + + _logger.LogInformation($"Successfully deleted user with id: {id}"); + return true; + } + + private async Task ValidateUniqueEmailAndUsername(string? email, string? username, Guid excludeUserId) + { + var existingUsers = await _unitOfWork.GetRepository().GetListAsync(); + + if (!string.IsNullOrEmpty(email)) { - throw new NotImplementedException(); + var existingUserWithEmail = existingUsers.FirstOrDefault(u => + u.Email.ToLower() == email.ToLower() && u.Id != excludeUserId); + + if (existingUserWithEmail != null) + { + _logger.LogWarning($"Email {email} is already taken by another user"); + throw new InvalidOperationException("Email is already taken by another user"); + } } - public Task UpdateUser(Guid id, UserResponse user) + if (!string.IsNullOrEmpty(username)) { - throw new NotImplementedException(); + var existingUserWithUsername = existingUsers.FirstOrDefault(u => + u.Username.ToLower() == username.ToLower() && u.Id != excludeUserId); + + if (existingUserWithUsername != null) + { + _logger.LogWarning($"Username {username} is already taken by another user"); + throw new InvalidOperationException("Username is already taken by another user"); + } } } } \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IAIGenService.cs b/TaskGenix.BLL/Services/Interfaces/IAIGenService.cs new file mode 100644 index 0000000..6a0d17a --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IAIGenService.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.AIModel; +using TaskGenix.Core.Requests.TaskGen; + +namespace TaskGenix.BLL.Services.Interfaces +{ + public interface IAIGenService + { + Task SummarizeTranscriptAsync(CreateSummaryRequest request); + + Task> GenerateTasksFromSummaryAsync(GenerateTasksRequest request); + + Task<(string Summary, List Tasks)> ProcessTranscriptToTasksAsync(CreateSummaryRequest request); + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IAuthService.cs b/TaskGenix.BLL/Services/Interfaces/IAuthService.cs new file mode 100644 index 0000000..2c131ca --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IAuthService.cs @@ -0,0 +1,16 @@ +using TaskGenix.Core.Requests.Auth; +using TaskGenix.Core.Responses.Auth; + +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IAuthService +{ + Task LoginAsync(LoginRequest request); + Task RegisterAsync(RegisterRequest request); + Task LogoutAsync(); + Task RefreshTokenAsync(string refreshToken); + Task RequestOtpAsync(string email); + Task VerifyOtpAsync(VerifyOtpRequest request); + Task ResendOtpAsync(string email); + Task OnboardAuth0UserAsync(Auth0OnboardingRequest request); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IEmailService.cs b/TaskGenix.BLL/Services/Interfaces/IEmailService.cs new file mode 100644 index 0000000..3bf993f --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IEmailService.cs @@ -0,0 +1,8 @@ +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IEmailService +{ + Task SendOtpEmailAsync(string toEmail, string otpCode); + Task SendLoginSuccessEmailAsync(string toEmail); + Task SendNotificationEmailAsync(string toEmail, string subject, string body); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IFileAccessService.cs b/TaskGenix.BLL/Services/Interfaces/IFileAccessService.cs new file mode 100644 index 0000000..623471f --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IFileAccessService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TaskGenix.Core.Domains; + +namespace TaskGenix.BLL.Services.Interfaces +{ + public interface IFileAccessService + { + Task GeneratePresignedUrlAsync(Recording recording, TimeSpan expiration); + Task GeneratePresignedUrlAsync(string bucketName, string objectKey, TimeSpan expiration); + Task DownloadFileAsync(Recording recording); + Task GetFileStreamAsync(Recording recording); + Task GetTemporaryFilePathAsync(Recording recording); + Task UploadFileAsync(string bucketName, string objectKey, Stream fileStream, string contentType); + } +} diff --git a/TaskGenix.BLL/Services/Interfaces/IJwtService.cs b/TaskGenix.BLL/Services/Interfaces/IJwtService.cs new file mode 100644 index 0000000..c4743f5 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IJwtService.cs @@ -0,0 +1,13 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using TaskGenix.Core.Domains; + +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IJwtService +{ + string GenerateToken(User user); + string GenerateRefreshToken(User user); + ClaimsPrincipal DecodeToken(string token); + JwtSecurityToken ReadTokenWithoutValidation(string token); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IJwtTokenService.cs b/TaskGenix.BLL/Services/Interfaces/IJwtTokenService.cs new file mode 100644 index 0000000..5270ee8 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IJwtTokenService.cs @@ -0,0 +1,13 @@ +using TaskGenix.Core.Domains; + +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IJwtTokenService +{ + Task SaveTokenAsync(User user, string accessToken, string refreshToken, bool isMobile = false); + Task ValidateRefreshTokenAsync(string refreshToken); + Task InvalidateTokenAsync(string refreshToken); + Task InvalidateAllUserTokensAsync(Guid userId); + Task GetTokenByRefreshTokenAsync(string refreshToken); + Task CleanupExpiredTokensAsync(); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/INotificationService.cs b/TaskGenix.BLL/Services/Interfaces/INotificationService.cs new file mode 100644 index 0000000..1aae5b7 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/INotificationService.cs @@ -0,0 +1,18 @@ +using TaskGenix.Core.Domains; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Notification; +using TaskGenix.Core.Responses.Notification; + +namespace TaskGenix.BLL.Services.Interfaces; + +public interface INotificationService +{ + Task SendMessageAsync(string deviceToken, string title, string body); + Task> GetAllNotificationsAsync(int page = 1, int size = 20); + Task> GetNotificationsByEmailAsync(string email); + Task CreateNotificationAsync(Guid userId, CreateNotificationRequest request); + Task MarkAsReadAsync(Guid notificationId); + Task SendPushNotificationAsync(string token, string title, string body); + Task SendNotificationToUserAsync(string email, string title, string body, NotificationType type); + Task SaveNotificationForUserAsync(string email, string title, string description, NotificationType type); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IOtpQuotaService.cs b/TaskGenix.BLL/Services/Interfaces/IOtpQuotaService.cs new file mode 100644 index 0000000..bb0d976 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IOtpQuotaService.cs @@ -0,0 +1,9 @@ +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IOtpQuotaService +{ + Task CanRequestOtpAsync(string email); + Task IncrementOtpRequestAsync(string email); + Task GetRemainingQuotaAsync(string email); + Task ResetQuotaAsync(string email); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IOtpService.cs b/TaskGenix.BLL/Services/Interfaces/IOtpService.cs new file mode 100644 index 0000000..4f16ae3 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IOtpService.cs @@ -0,0 +1,13 @@ +using TaskGenix.Core.Domains; + +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IOtpService +{ + Task GenerateOtpAsync(string email, OtpPurpose purpose); + Task ValidateOtpAsync(string email, string otp, OtpPurpose purpose); + Task InvalidateOtpAsync(string email, OtpPurpose purpose); + Task CanRequestOtpAsync(string email); + Task GetRemainingAttemptsAsync(string email); + Task CleanupExpiredOtpsAsync(); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IPaymentService.cs b/TaskGenix.BLL/Services/Interfaces/IPaymentService.cs new file mode 100644 index 0000000..58fd3f4 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IPaymentService.cs @@ -0,0 +1,19 @@ +using TaskGenix.Core.Domains; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Payment; +using TaskGenix.Core.Responses.PaymentResponse; + +namespace TaskGenix.BLL.Services.Interfaces +{ + public interface IPaymentService + { + //Task ProcessPaymentAsync(PaymentRequest request); + Task> GetAllPaymentTransactionsAsync(); + Task CreatePaymentLinkAsync(PaymentRequest request); + Task HandleWebhookAsync(object webhookData); + Task CancelPaymentAsync(string orderCode); + Task HandlePaymentSuccessAsync(string orderCode); + Task VerifyIapAsync(VerifyIapRequest request); + //Task VerifyPaymentAsync(string orderCode); + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IPlanService.cs b/TaskGenix.BLL/Services/Interfaces/IPlanService.cs index 70c4a78..50365fc 100644 --- a/TaskGenix.BLL/Services/Interfaces/IPlanService.cs +++ b/TaskGenix.BLL/Services/Interfaces/IPlanService.cs @@ -4,14 +4,13 @@ using System.Threading.Tasks; using TaskGenix.Core.Responses.Plan; -namespace TaskGenix.BLL.Services.Interfaces +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IPlanService { - public interface IPlanService - { - Task> GetAllPlans(); - Task GetPlanById(Guid id); - Task CreatePlan(PlanResponse plan); - Task UpdatePlan(Guid id, PlanResponse plan); - Task DeletePlan(Guid id); - } + Task> GetAllPlans(); + Task GetPlanById(Guid id); + Task CreatePlan(PlanResponse plan); + Task UpdatePlan(Guid id, PlanResponse plan); + Task DeletePlan(Guid id); } \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IProjectMemberService.cs b/TaskGenix.BLL/Services/Interfaces/IProjectMemberService.cs new file mode 100644 index 0000000..d0a4c38 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IProjectMemberService.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.ProjectMember; +using TaskGenix.Core.Responses.Project; + +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IProjectMemberService +{ + // DTO response methods (main interface for controllers) + Task> GetMembersByProjectId(Guid projectId); + Task AddMemberToProject(Guid projectId, AddProjectMemberRequest request); + Task UpdateProjectMember(Guid projectId, Guid userId, UpdateProjectMemberRequest request); + Task RemoveMemberFromProject(Guid projectId, Guid userId); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IProjectProgressCalculator.cs b/TaskGenix.BLL/Services/Interfaces/IProjectProgressCalculator.cs new file mode 100644 index 0000000..d05cd2e --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IProjectProgressCalculator.cs @@ -0,0 +1,19 @@ +using TaskGenix.Core.Domains; + +namespace TaskGenix.BLL.Services.Interfaces; + +/// +/// Service to calculate workspace (project) progress based on tasks (TaskGen) within it +/// +public interface IProjectProgressCalculator +{ + /// + /// Calculate simple workspace progress: completed tasks / total tasks * 100 + /// + decimal CalculateProgress(IEnumerable tasks); + + /// + /// Calculate weighted workspace progress based on task priority and status + /// + decimal CalculateWeightedProgress(IEnumerable tasks); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IProjectService.cs b/TaskGenix.BLL/Services/Interfaces/IProjectService.cs index 3d26653..b4655f7 100644 --- a/TaskGenix.BLL/Services/Interfaces/IProjectService.cs +++ b/TaskGenix.BLL/Services/Interfaces/IProjectService.cs @@ -3,23 +3,35 @@ using System.Linq; using System.Threading.Tasks; using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.Project; using TaskGenix.Core.Responses.Project; -namespace TaskGenix.BLL.Services.Interfaces +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IProjectService { - public interface IProjectService - { - Task> GetAllProjects(); - Task> GetProjectsByOwnerId(Guid ownerId); - Task> GetProjectsByUserId(Guid userId); - Task GetProjectById(Guid projectId); - Task CreateProject(Project project); - Task UpdateProject(Project project); - Task DeleteProject(Guid projectId); + Task> GetAllProjects(); + Task> GetProjectsByOwnerId(Guid ownerId); + Task> GetProjectsByOwnerIdAndStatus(Guid ownerId, ProjectStatus status); + + /// + /// Get all workspaces (projects) that user is member of, with calculated progress + /// + Task> GetProjectsByUserId(Guid userId); + + Task GetProjectById(Guid projectId); + Task CreateProject(CreateProjectRequest project); + Task UpdateProject(Guid projectId, UpdateProjectRequest request); + Task DeleteProject(Guid projectId); + + // Progress-specific methods for better performance + /// + /// Get detailed progress for a specific workspace + /// + Task GetProjectProgress(Guid projectId); - Task> GetAllProjectsDto(); - Task> GetProjectsByOwnerIdDto(Guid ownerId); - Task> GetProjectsByUserIdDto(Guid userId); - Task GetProjectByIdDto(Guid projectId); - } + /// + /// Get progress summary for all user's workspaces - optimized for dashboard + /// + Task> GetProjectsProgressByUserId(Guid userId); } \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IRecordingService.cs b/TaskGenix.BLL/Services/Interfaces/IRecordingService.cs new file mode 100644 index 0000000..e3255bd --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IRecordingService.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.Recording; +using TaskGenix.Core.Responses.Recording; + +namespace TaskGenix.BLL.Services.Interfaces +{ + public interface IRecordingService + { + Task UploadRecordingAsync( + Stream recordingStream, + long contentLength, + string title, + string ownerEmail, + string? mimeType, + TimeSpan? duration, + string? transcriptText = null + ); + + Task GetRecordingByIdAsync(Guid recordingId); + Task GetRecordingMetaAsync(Guid recordingId); + + // New CRUD methods + Task> GetAllRecordings(); + Task GetRecordingResponseByIdAsync(Guid recordingId); + Task CreateRecording(CreateRecordingRequest request); + Task UpdateRecording(Guid recordingId, UpdateRecordingRequest request); + Task DeleteRecording(Guid recordingId); + Task> GetRecordingsByUserIdAsync(Guid userId); + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/ISystemStatusService.cs b/TaskGenix.BLL/Services/Interfaces/ISystemStatusService.cs new file mode 100644 index 0000000..974ec17 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/ISystemStatusService.cs @@ -0,0 +1,24 @@ +using TaskGenix.Core.Responses.SystemStatus; + +namespace TaskGenix.BLL.Services.Interfaces; + +/// +/// Service for checking system and service health status +/// +public interface ISystemStatusService +{ + /// + /// Get overall system status including all services + /// + Task GetSystemStatusAsync(); + + /// + /// Check specific service status by name + /// + Task CheckServiceStatusAsync(string serviceName); + + /// + /// Get list of all available services + /// + Task> GetAvailableServicesAsync(); +} diff --git a/TaskGenix.BLL/Services/Interfaces/ITaskAssignmentService.cs b/TaskGenix.BLL/Services/Interfaces/ITaskAssignmentService.cs new file mode 100644 index 0000000..a3a776e --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/ITaskAssignmentService.cs @@ -0,0 +1,58 @@ +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.TaskAssignment; +using TaskGenix.Core.Responses.TaskGen; + +namespace TaskGenix.BLL.Services.Interfaces; + +public interface ITaskAssignmentService +{ + /// + /// Assign a user to a task + /// + Task AssignUserToTaskAsync(AssignUserToTaskRequest request); + + /// + /// Assign multiple users to a task at once + /// + Task> BulkAssignUsersToTaskAsync(BulkAssignUsersRequest request); + + /// + /// Remove a user from a task + /// + Task RemoveUserFromTaskAsync(RemoveUserFromTaskRequest request); + + /// + /// Get all assignments for a specific task + /// + Task> GetAssignmentsByTaskIdAsync(Guid taskId); + + /// + /// Get all tasks assigned to a specific user + /// + Task> GetAssignmentsByUserIdAsync(Guid userId); + + /// + /// Get all tasks assigned to the current logged-in user + /// + Task> GetMyAssignmentsAsync(); + + /// + /// Get a specific assignment by ID + /// + Task GetAssignmentByIdAsync(Guid assignmentId); + + /// + /// Check if a user is assigned to a task + /// + Task IsUserAssignedToTaskAsync(Guid taskId, Guid userId); + + /// + /// Remove all assignments for a task + /// + Task RemoveAllAssignmentsForTaskAsync(Guid taskId); + + /// + /// Get all available users (project members) that can be assigned to a task + /// + Task> GetAvailableUsersForTaskAsync(Guid taskId); +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/ITaskGenService.cs b/TaskGenix.BLL/Services/Interfaces/ITaskGenService.cs index 19b4ec1..547a915 100644 --- a/TaskGenix.BLL/Services/Interfaces/ITaskGenService.cs +++ b/TaskGenix.BLL/Services/Interfaces/ITaskGenService.cs @@ -6,26 +6,19 @@ using TaskGenix.Core.Requests.TaskGen; using TaskGenix.Core.Responses.TaskGen; -namespace TaskGenix.BLL.Services.Interfaces -{ - public interface ITaskGenService - { - // Original methods returning domain entities - Task> GetAllTasks(); - Task> GetTasksByProjectId(Guid projectId); - Task> GetTasksByUserId(Guid userId); - Task GetTaskById(Guid taskId); - Task CreateTask(TaskGen task); - Task> CreateTaskBulk(IEnumerable tasks); - Task> UpdateTaskBulk(IEnumerable tasks); - Task UpdateTask(TaskGen task); - Task DeleteTask(Guid taskId); +namespace TaskGenix.BLL.Services.Interfaces; - // New methods returning DTOs to avoid JSON cycles - Task> GetAllTasksDto(); - Task> GetTasksByProjectIdDto(Guid projectId); - Task> GetTasksByUserIdDto(Guid userId); - Task GetTaskByIdDto(Guid taskId); +public interface ITaskGenService +{ + Task> GetAllTasks(); + Task> GetTasksByProjectId(Guid projectId); + Task> GetTasksByUserId(Guid userId); + Task GetTaskById(Guid taskId); + Task CreateTask(CreateTaskRequest request); + Task> CreateTaskBulk(IEnumerable requests); + Task UpdateTask(Guid taskId, UpdateTaskRequest request); + Task DeleteTask(Guid taskId); - } + Task AssignTaskToProject(Guid taskId, Guid projectId); + Task UnassignTaskFromProject(Guid taskId); } \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IUserDeviceTokenService.cs b/TaskGenix.BLL/Services/Interfaces/IUserDeviceTokenService.cs new file mode 100644 index 0000000..b0c7fe6 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IUserDeviceTokenService.cs @@ -0,0 +1,20 @@ +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.UserDeviceToken; +using TaskGenix.Core.Responses.UserDeviceToken; + +namespace TaskGenix.BLL.Services.Interfaces +{ + public interface IUserDeviceTokenService + { + Task> GetAllTokensAsync(int page = 1, int size = 20); + Task GetTokenByIdAsync(Guid id); + Task CreateOrUpdateTokenAsyncV2(CreateUserDeviceTokenRequestV2 request); + Task UpdateTokenAsync(Guid id, UpdateUserDeviceTokenRequest request); + Task> GetFcmTokensByUserEmailAsync(string email); + Task DeleteTokenAsync(Guid id); + + // New methods for handling timing issues + Task CreateOrUpdateTokenWithEmailAsync(string email, CreateUserDeviceTokenRequestV2 request); + Task CleanupExpiredTokensAsync(int olderThanDays = 90); + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IUserPlanFeatureService.cs b/TaskGenix.BLL/Services/Interfaces/IUserPlanFeatureService.cs new file mode 100644 index 0000000..35d0c5f --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IUserPlanFeatureService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TaskGenix.Core.Requests.UserPlanFeature; +using TaskGenix.Core.Responses.UserPlanFeature; + +namespace TaskGenix.BLL.Services.Interfaces +{ + public interface IUserPlanFeatureService + { + Task CreateUserPlanFeature(CreateUserPlanFeatureRequest request); + Task> GetUserPlanFeaturesByUserPlanId(Guid userPlanId); + Task UpdateUserPlanFeature(UpdateUserPlanFeatureRequest request); + Task DeleteUserPlanFeature(Guid id); + } +} diff --git a/TaskGenix.BLL/Services/Interfaces/IUserPlanService.cs b/TaskGenix.BLL/Services/Interfaces/IUserPlanService.cs new file mode 100644 index 0000000..4714226 --- /dev/null +++ b/TaskGenix.BLL/Services/Interfaces/IUserPlanService.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TaskGenix.Core.Requests.UserPlan; +using TaskGenix.Core.Responses.UserPlan; + +namespace TaskGenix.BLL.Services.Interfaces +{ + public interface IUserPlanService + { + Task> GetAllUserPlans(); + Task GetUserPlanById(Guid id); + Task CreateUserPlan(UserPlanRequest userPlanRequest); + Task UpdateUserPlan(Guid id, UpdateUserPlanRequest updateUserPlanRequest); + Task DeleteUserPlan(Guid id); + } +} \ No newline at end of file diff --git a/TaskGenix.BLL/Services/Interfaces/IUserService.cs b/TaskGenix.BLL/Services/Interfaces/IUserService.cs index be3aa32..229e9a3 100644 --- a/TaskGenix.BLL/Services/Interfaces/IUserService.cs +++ b/TaskGenix.BLL/Services/Interfaces/IUserService.cs @@ -2,16 +2,19 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using TaskGenix.Core.Meta; using TaskGenix.Core.Responses.User; -namespace TaskGenix.BLL.Services.Interfaces +namespace TaskGenix.BLL.Services.Interfaces; + +public interface IUserService { - public interface IUserService - { - Task> GetAllUsers(); - Task GetUserById(Guid id); - Task CreateUser(UserResponse user); - Task UpdateUser(Guid id, UserResponse user); - Task DeleteUser(Guid id); - } + Task> GetAllUsers(); + Task> GetUsersPaged(int page, int pageSize); + Task GetUserById(Guid id); + Task GetUserByAuth0Id(string auth0id); + Task CreateUser(UserRequest user); + Task UpdateUser(Guid id, UpdateUserRequest userRequest); + Task UpdatePartialUser(Guid id, UpdatePartialUserRequest userRequest); + Task DeleteUser(Guid id); } \ No newline at end of file diff --git a/TaskGenix.BLL/TaskGenix.BLL.csproj b/TaskGenix.BLL/TaskGenix.BLL.csproj index cd0b0ae..c585a16 100644 --- a/TaskGenix.BLL/TaskGenix.BLL.csproj +++ b/TaskGenix.BLL/TaskGenix.BLL.csproj @@ -1,13 +1,21 @@ - - - - net8.0 - enable - enable - - - - - - - + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/TaskGenix.Core/Constants/CommonMessage.cs b/TaskGenix.Core/Constants/CommonMessage.cs new file mode 100644 index 0000000..bd89aa8 --- /dev/null +++ b/TaskGenix.Core/Constants/CommonMessage.cs @@ -0,0 +1,28 @@ +namespace TaskGenix.Core.Constants; + +public class CommonMessage +{ + public const string NotFound = "Resource not found"; + public const string Unauthorized = "Unauthorized access"; + public const string BadRequest = "Bad request"; + public const string InternalServerError = "An unexpected error occurred"; + public const string ValidationError = "Validation error"; + public const string GeneralError = "An error occurred"; + public const string OperationSuccessful = "Operation completed successfully"; +} + +public class CommonExceptionMessage +{ + public const string UserNotFound = "User not found"; + public const string InvalidCredentials = "Invalid email or password"; + public const string EmailAlreadyInUse = "Email is already in use"; + public const string UsernameAlreadyInUse = "Username is already in use"; + public const string TokenExpired = "Token has expired"; + public const string InvalidToken = "Invalid token"; + public const string OtpSendFailed = "Failed to send OTP. Please try again later."; + public const string OtpVerificationFailed = "OTP verification failed. Please try again later."; + public const string OtpResendFailed = "Failed to resend OTP. Please try again later."; + public const string OtpInvalid = "Invalid OTP"; + public const string OtpExpired = "OTP has expired"; + public const string OtpResendLimitExceeded = "OTP resend limit exceeded. Please try again later."; +} \ No newline at end of file diff --git a/TaskGenix.Core/Constants/TemplateEmail.cs b/TaskGenix.Core/Constants/TemplateEmail.cs new file mode 100644 index 0000000..9d0f7fd --- /dev/null +++ b/TaskGenix.Core/Constants/TemplateEmail.cs @@ -0,0 +1,310 @@ +namespace TaskGenix.Core.Constants +{ + public class TemplateEmail + { + public const string OtpTemplate = @" + + + + + + TaskGenix - OTP + + + + + + + +
+
+
TaskGenix
+

Xác thực đăng nhập

+

Mã OTP của bạn là:

+ +
{{OtpCode}}
+ +

Mã sẽ hết hạn sau 5 phút.

+

+ Nếu bạn không yêu cầu mã này, có thể ai đó đã nhập sai email của họ. + Vui lòng bỏ qua hoặc liên hệ hỗ trợ tại
+ {{SupportEmail}}. +

+ +
© {{Year}} TaskGenix
+
+
+ + +"; + + public const string LoginSuccessTemplate = @" + + + + + + TaskGenix - Đăng nhập thành công + + + + + + + +
+
+
TaskGenix
+

Bạn vừa đăng nhập

+

Chúng tôi ghi nhận một lượt đăng nhập mới vào tài khoản của bạn.

+ +
+ Thời gian: {{LoginTime}}
+ Thiết bị / IP: {{DeviceInfo}} / {{IpAddress}}
+ Địa điểm (ước lượng): {{Location}} +
+ +

+ Nếu đó không phải bạn, hãy đặt lại mật khẩu + hoặc liên hệ hỗ trợ: {{SupportEmail}}. +

+ +
© {{Year}} TaskGenix
+
+
+ + +"; + + public const string NotificationTemplate = @" + + + + + + {{Subject}} + + + + + + + +
+
+
TaskGenix
+

{{Title}}

+
{{BodyHtml}}
+ + {{#IfButton}} + + {{/IfButton}} + + +
+
+ + +"; + + + + } +} diff --git a/TaskGenix.Core/Converters/ClassStringEnumConverter.cs b/TaskGenix.Core/Converters/ClassStringEnumConverter.cs index ebd1e74..7076db5 100644 --- a/TaskGenix.Core/Converters/ClassStringEnumConverter.cs +++ b/TaskGenix.Core/Converters/ClassStringEnumConverter.cs @@ -1,30 +1,26 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace TaskGenix.Core.Converters +namespace TaskGenix.Core.Converters; + +public class StringEnumConverter : JsonConverter where T : struct, Enum { - public class StringEnumConverter : JsonConverter where T : struct, Enum + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException($"Invalid data type for {typeof(T).Name}. Expected a string."); - } + if (reader.TokenType != JsonTokenType.String) + throw new JsonException($"Invalid data type for {typeof(T).Name}. Expected a string."); - string? value = reader.GetString(); + var value = reader.GetString(); - if (Enum.TryParse(value, true, out T result)) // Case-insensitive parsing - { - return result; - } + if (Enum.TryParse(value, true, out T result)) // Case-insensitive parsing + return result; - throw new JsonException($"Invalid value '{value}' for {typeof(T).Name}. Allowed values: {string.Join(", ", Enum.GetNames(typeof(T)))}."); - } + throw new JsonException( + $"Invalid value '{value}' for {typeof(T).Name}. Allowed values: {string.Join(", ", Enum.GetNames(typeof(T)))}."); + } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToString()); - } + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Converters/DateOnlyConverter.cs b/TaskGenix.Core/Converters/DateOnlyConverter.cs index 6c23884..eede85d 100644 --- a/TaskGenix.Core/Converters/DateOnlyConverter.cs +++ b/TaskGenix.Core/Converters/DateOnlyConverter.cs @@ -2,33 +2,30 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace TaskGenix.Core.Converters +namespace TaskGenix.Core.Converters; + +public class DateOnlyConverter : JsonConverter { - public class DateOnlyConverter : JsonConverter - { - private readonly string[] _supportedFormats = - [ - "yyyy/MM/dd", - "dd/MM/yyyy", - "yyyy-MM-dd", - "dd-MM-yyyy" - ]; + private readonly string[] _supportedFormats = + [ + "yyyy/MM/dd", + "dd/MM/yyyy", + "yyyy-MM-dd", + "dd-MM-yyyy" + ]; - public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var dateString = reader.GetString(); + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var dateString = reader.GetString(); - if (DateOnly.TryParseExact(dateString, _supportedFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) - { - return date; - } + if (DateOnly.TryParseExact(dateString, _supportedFormats, CultureInfo.InvariantCulture, DateTimeStyles.None, + out var date)) return date; - throw new JsonException($"Invalid date format. Use [{string.Join(", ", _supportedFormats)}]."); - } + throw new JsonException($"Invalid date format. Use [{string.Join(", ", _supportedFormats)}]."); + } - public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.ToString("yyyy-MM-dd")); // Standardize response format - } + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString("yyyy-MM-dd")); // Standardize response format } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Converters/GuidNullableConverter.cs b/TaskGenix.Core/Converters/GuidNullableConverter.cs index 88a2ed0..62ba3ae 100644 --- a/TaskGenix.Core/Converters/GuidNullableConverter.cs +++ b/TaskGenix.Core/Converters/GuidNullableConverter.cs @@ -1,27 +1,20 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace TaskGenix.Core.Converters +namespace TaskGenix.Core.Converters; + +public class GuidNullableConverter : JsonConverter { - public class GuidNullableConverter : JsonConverter + public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override Guid? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - var value = reader.GetString(); - if (string.IsNullOrEmpty(value)) - { - return null; - } - if (Guid.TryParse(value, out var guid)) - { - return guid; - } - throw new ArgumentException($"Invalid GUID format: {value}"); - } + var value = reader.GetString(); + if (string.IsNullOrEmpty(value)) return null; + if (Guid.TryParse(value, out var guid)) return guid; + throw new ArgumentException($"Invalid GUID format: {value}"); + } - public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options) - { - writer.WriteStringValue(value.Value.ToString()); - } + public override void Write(Utf8JsonWriter writer, Guid? value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.Value.ToString()); } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Domains/Attachment.cs b/TaskGenix.Core/Domains/Attachment.cs index d43459e..f45e496 100644 --- a/TaskGenix.Core/Domains/Attachment.cs +++ b/TaskGenix.Core/Domains/Attachment.cs @@ -10,6 +10,9 @@ namespace TaskGenix.Core.Domains [Table("Attachments")] public class Attachment : BaseEntity { + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); [Required] [MaxLength(255)] @@ -19,11 +22,7 @@ public class Attachment : BaseEntity public byte[] Data { get; set; } = null!; - [ForeignKey("TaskGen")] - public Guid TaskGenId { get; set; } + [ForeignKey("TaskGen")] public Guid TaskGenId { get; set; } public TaskGen TaskGen { get; set; } = null!; - - - } } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/BaseEntity.cs b/TaskGenix.Core/Domains/BaseEntity.cs index db9b5eb..d265fb6 100644 --- a/TaskGenix.Core/Domains/BaseEntity.cs +++ b/TaskGenix.Core/Domains/BaseEntity.cs @@ -2,18 +2,11 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace TaskGenix.Core.Domains -{ - public abstract class BaseEntity - { - [Key] - [Required] - public Guid Id { get; set; } = Guid.NewGuid(); +namespace TaskGenix.Core.Domains; - [Required] - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; +public abstract class BaseEntity +{ + [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - [Required] - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - } + [Required] public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/Feature.cs b/TaskGenix.Core/Domains/Feature.cs index a394e3a..9028627 100644 --- a/TaskGenix.Core/Domains/Feature.cs +++ b/TaskGenix.Core/Domains/Feature.cs @@ -10,25 +10,21 @@ namespace TaskGenix.Core.Domains [Table("Features")] public class Feature : BaseEntity { - + [Key] [Required] - [MaxLength(50)] - public string Key { get; set; } = null!; + public Guid Id { get; set; } = Guid.NewGuid(); - [Required] - [MaxLength(100)] - public string Name { get; set; } = null!; + [Required][MaxLength(50)] public string Key { get; set; } = null!; - [Required] - [MaxLength(50)] - public string Unit { get; set; } = null!; + [Required][MaxLength(100)] public string Name { get; set; } = null!; - [Required] - public FeatureType FeatureType { get; set; } + [Required][MaxLength(50)] public string Unit { get; set; } = null!; + + [Required] public FeatureType FeatureType { get; set; } // Navigation property to plan features - public List PlanFeatures { get; set; } = new List(); + public List PlanFeatures { get; set; } = new(); } public enum FeatureType @@ -37,4 +33,4 @@ public enum FeatureType Decimal, Boolean } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Domains/JwtToken.cs b/TaskGenix.Core/Domains/JwtToken.cs new file mode 100644 index 0000000..23d6d76 --- /dev/null +++ b/TaskGenix.Core/Domains/JwtToken.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace TaskGenix.Core.Domains; + +[Table("JwtTokens")] +public class JwtToken : BaseEntity +{ + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public string AccessToken { get; set; } = null!; + + [Required] public string RefreshToken { get; set; } = null!; + + [Required] public DateTime ExpirationDate { get; set; } + + [Required] public DateTime RefreshTokenExpirationDate { get; set; } + + public bool IsMobile { get; set; } = false; + + public bool IsRevoked { get; set; } = false; + + public bool IsExpired { get; set; } = false; + + [MaxLength(100)] public string? DeviceInfo { get; set; } + + [MaxLength(45)] public string? IpAddress { get; set; } + + // Foreign key property + [Required] public Guid UserId { get; set; } + + // Navigation property + [ForeignKey(nameof(UserId))] public User User { get; set; } = null!; +} \ No newline at end of file diff --git a/TaskGenix.Core/Domains/Notification.cs b/TaskGenix.Core/Domains/Notification.cs index 1ee0c02..4d1ff91 100644 --- a/TaskGenix.Core/Domains/Notification.cs +++ b/TaskGenix.Core/Domains/Notification.cs @@ -10,6 +10,9 @@ namespace TaskGenix.Core.Domains [Table("Notifications")] public class Notification : BaseEntity { + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); [Required] [MaxLength(200)] @@ -21,6 +24,12 @@ public class Notification : BaseEntity public NotificationType Type { get; set; } + public NotificationIcon? Icon { get; set; } = NotificationIcon.MESSAGE; + public NotificationColor? Color { get; set; } = NotificationColor.BLUE; + + [EmailAddress] + public string Email { get; set; } = null!; + public bool IsSent { get; set; } = false; public bool IsRead { get; set; } = false; @@ -44,51 +53,44 @@ public class Notification : BaseEntity public DeliveryStatus? DeliveryStatus { get; set; } - public DeliveryChannel DeliveryChannel { get; set; } = DeliveryChannel.InApp; - - - [Required] - public Guid UserId { get; set; } - public User User { get; set; } = null!; - - } public enum NotificationType { - Information, - Reminder, - DueDate, - SystemAlert, - Urgent, - TaskAssigned, - TaskUpdated, - TaskCompleted, - TaskOverdue, - PlanExpiring + INFO, + WARNING, + SUCCESS, + ERROR, } - public enum ReminderPattern + public enum NotificationIcon { - Once, - Daily, - BeforeDueDate, - Custom + MESSAGE, + ALERT, + CHECK, + ERROR } - public enum DeliveryStatus + public enum NotificationColor { - Pending, - Sent, - Failed + BLUE, + YELLOW, + GREEN, + RED } - public enum DeliveryChannel + public enum ReminderPattern + { + ONCE, + DAILY, + BEFORE_DUE_DATE, + CUSTOM + } + + public enum DeliveryStatus { - InApp, - Email, - PushNotification, - SMS, - All + PENDING, + SENT, + FAILED } } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/NotificationProcessor.cs b/TaskGenix.Core/Domains/NotificationProcessor.cs index 4598a3c..de9c5e1 100644 --- a/TaskGenix.Core/Domains/NotificationProcessor.cs +++ b/TaskGenix.Core/Domains/NotificationProcessor.cs @@ -3,141 +3,138 @@ using System.Linq; using System.Threading.Tasks; -namespace TaskGenix.Core.Domains +namespace TaskGenix.Core.Domains; + +/// +/// Xử lý việc tạo và gửi các thông báo nhắc nhở cho các task +/// +public class NotificationProcessor { /// - /// Xử lý việc tạo và gửi các thông báo nhắc nhở cho các task + /// Tạo chuỗi thông báo nhắc nhở cho một task /// - public class NotificationProcessor + public static List GenerateTaskReminderSeries(TaskGen task, User assignee) { - /// - /// Tạo chuỗi thông báo nhắc nhở cho một task - /// - public static List GenerateTaskReminderSeries(TaskGen task, User assignee) - { - // Nếu không có deadline hoặc không cấu hình nhắc nhở - if (!task.DueDate.HasValue || task.ReminderConfig == null || !task.ReminderConfig.EnableReminders) - return new List(); + // Nếu không có deadline hoặc không cấu hình nhắc nhở + if (!task.DueDate.HasValue || task.ReminderConfig == null || !task.ReminderConfig.EnableReminders) + return new List(); - DateTime dueDate = task.DueDate.Value; - var config = task.ReminderConfig; - var reminders = new List(); + var dueDate = task.DueDate.Value; + var config = task.ReminderConfig; + var reminders = new List(); - // Tạo một ID chung cho chuỗi nhắc nhở này - Guid reminderSeriesId = Guid.NewGuid(); + // Tạo một ID chung cho chuỗi nhắc nhở này + var reminderSeriesId = Guid.NewGuid(); - // Tính ngày bắt đầu gửi nhắc nhở - DateTime startDate = dueDate.AddDays(-config.StartRemindingDaysBeforeDue); - if (startDate < DateTime.UtcNow) - startDate = DateTime.UtcNow; + // Tính ngày bắt đầu gửi nhắc nhở + var startDate = dueDate.AddDays(-config.StartRemindingDaysBeforeDue); + if (startDate < DateTime.UtcNow) + startDate = DateTime.UtcNow; - int sequenceNumber = 1; + var sequenceNumber = 1; - // Tạo các thông báo nhắc nhở theo tần suất cấu hình - for (DateTime currentDate = startDate; - currentDate <= dueDate; - currentDate = currentDate.AddDays(config.ReminderFrequencyInDays)) - { - // Tạo giờ gửi thông báo theo cấu hình - DateTime scheduledTime = new DateTime( - currentDate.Year, currentDate.Month, currentDate.Day, - config.PreferredReminderHour, 0, 0, DateTimeKind.Utc); - - // Bỏ qua nếu thời gian đã qua - if (scheduledTime < DateTime.UtcNow) - continue; - - // Xác định độ ưu tiên dựa trên độ gần với deadline - var daysUntilDue = (dueDate - currentDate).TotalDays; - var type = GetNotificationTypeByDaysRemaining(daysUntilDue, config.EscalateReminderPriority); - - // Tạo nội dung thông báo - string title = GetReminderTitle(task, daysUntilDue); - string message = GetReminderMessage(task, daysUntilDue); - - var notification = new Notification - { - Title = title, - Message = message, - TaskGenId = task.Id, - UserId = assignee.Id, - Type = type, - ReminderPattern = ReminderPattern.BeforeDueDate, - ReminderSeriesId = reminderSeriesId, - SequenceNumber = sequenceNumber++, - ScheduledTime = scheduledTime, - DeliveryChannel = config.PreferredChannel, - CreatedAt = DateTime.UtcNow - }; - - reminders.Add(notification); - } - - // Thêm thông báo đúng ngày deadline - reminders.Add(new Notification + // Tạo các thông báo nhắc nhở theo tần suất cấu hình + for (var currentDate = startDate; + currentDate <= dueDate; + currentDate = currentDate.AddDays(config.ReminderFrequencyInDays)) + { + // Tạo giờ gửi thông báo theo cấu hình + var scheduledTime = new DateTime( + currentDate.Year, currentDate.Month, currentDate.Day, + config.PreferredReminderHour, 0, 0, DateTimeKind.Utc); + + // Bỏ qua nếu thời gian đã qua + if (scheduledTime < DateTime.UtcNow) + continue; + + // Xác định độ ưu tiên dựa trên độ gần với deadline + var daysUntilDue = (dueDate - currentDate).TotalDays; + var type = GetNotificationTypeByDaysRemaining(daysUntilDue, config.EscalateReminderPriority); + + // Tạo nội dung thông báo + var title = GetReminderTitle(task, daysUntilDue); + var message = GetReminderMessage(task, daysUntilDue); + + var notification = new Notification { - Title = $"HÔM NAY LÀ DEADLINE: {task.Title}", - Message = $"Task '{task.Title}' cần được hoàn thành trong hôm nay!", + Title = title, + Message = message, TaskGenId = task.Id, - UserId = assignee.Id, - Type = NotificationType.DueDate, - ReminderPattern = ReminderPattern.BeforeDueDate, + //UserId = assignee.Id, + Type = type, + ReminderPattern = ReminderPattern.DAILY, ReminderSeriesId = reminderSeriesId, - SequenceNumber = sequenceNumber, - ScheduledTime = new DateTime( - dueDate.Year, dueDate.Month, dueDate.Day, - config.PreferredReminderHour, 0, 0, DateTimeKind.Utc), - DeliveryChannel = DeliveryChannel.All, // Gửi qua tất cả các kênh cho deadline + SequenceNumber = sequenceNumber++, + ScheduledTime = scheduledTime, CreatedAt = DateTime.UtcNow - }); + }; - return reminders; + reminders.Add(notification); } - /// - /// Xác định loại thông báo dựa trên số ngày còn lại - /// - private static NotificationType GetNotificationTypeByDaysRemaining(double daysRemaining, bool escalate) + // Thêm thông báo đúng ngày deadline + reminders.Add(new Notification { - if (!escalate) - return NotificationType.Reminder; - - if (daysRemaining <= 1) - return NotificationType.Urgent; - else if (daysRemaining <= 2) - return NotificationType.DueDate; - else if (daysRemaining <= 5) - return NotificationType.Reminder; - else - return NotificationType.Information; - } + Title = $"HÔM NAY LÀ DEADLINE: {task.Title}", + Message = $"Task '{task.Title}' cần được hoàn thành trong hôm nay!", + TaskGenId = task.Id, + //UserId = assignee.Id, + Type = NotificationType.WARNING, + ReminderPattern = ReminderPattern.BEFORE_DUE_DATE, + ReminderSeriesId = reminderSeriesId, + SequenceNumber = sequenceNumber, + ScheduledTime = new DateTime( + dueDate.Year, dueDate.Month, dueDate.Day, + config.PreferredReminderHour, 0, 0, DateTimeKind.Utc), + CreatedAt = DateTime.UtcNow + }); + + return reminders; + } - /// - /// Tạo tiêu đề nhắc nhở tùy theo thời gian còn lại - /// - private static string GetReminderTitle(TaskGen task, double daysRemaining) - { - if (daysRemaining < 1) - return $"KHẨN CẤP: {task.Title} đến hạn hôm nay!"; - else if (daysRemaining == 1) - return $"DEADLINE MAI: {task.Title}"; - else - return $"Nhắc nhở: {task.Title} (còn {Math.Ceiling(daysRemaining)} ngày)"; - } + /// + /// Xác định loại thông báo dựa trên số ngày còn lại + /// + private static NotificationType GetNotificationTypeByDaysRemaining(double daysRemaining, bool escalate) + { + if (!escalate) + return NotificationType.INFO; + + if (daysRemaining <= 1) + return NotificationType.ERROR; + else if (daysRemaining <= 2) + return NotificationType.WARNING; + else if (daysRemaining <= 5) + return NotificationType.INFO; + else + return NotificationType.INFO; + } - /// - /// Tạo nội dung nhắc nhở tùy theo thời gian còn lại - /// - private static string GetReminderMessage(TaskGen task, double daysRemaining) - { - string timeRemaining = daysRemaining < 1 - ? "hôm nay" - : daysRemaining == 1 - ? "1 ngày nữa" - : $"{Math.Ceiling(daysRemaining)} ngày nữa"; - - return $"Task '{task.Title}' sẽ đến hạn trong {timeRemaining}. " + - $"Mô tả: {task.Description?.Substring(0, Math.Min(task.Description?.Length ?? 0, 100))}"; - } + /// + /// Tạo tiêu đề nhắc nhở tùy theo thời gian còn lại + /// + private static string GetReminderTitle(TaskGen task, double daysRemaining) + { + if (daysRemaining < 1) + return $"KHẨN CẤP: {task.Title} đến hạn hôm nay!"; + else if (daysRemaining == 1) + return $"DEADLINE MAI: {task.Title}"; + else + return $"Nhắc nhở: {task.Title} (còn {Math.Ceiling(daysRemaining)} ngày)"; + } + + /// + /// Tạo nội dung nhắc nhở tùy theo thời gian còn lại + /// + private static string GetReminderMessage(TaskGen task, double daysRemaining) + { + var timeRemaining = daysRemaining < 1 + ? "hôm nay" + : daysRemaining == 1 + ? "1 ngày nữa" + : $"{Math.Ceiling(daysRemaining)} ngày nữa"; + + return $"Task '{task.Title}' sẽ đến hạn trong {timeRemaining}. " + + $"Mô tả: {task.Description?.Substring(0, Math.Min(task.Description?.Length ?? 0, 100))}"; } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Domains/OtpCode.cs b/TaskGenix.Core/Domains/OtpCode.cs new file mode 100644 index 0000000..5accbb8 --- /dev/null +++ b/TaskGenix.Core/Domains/OtpCode.cs @@ -0,0 +1,41 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace TaskGenix.Core.Domains; + +[Table("OtpCodes")] +public class OtpCode : BaseEntity +{ + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + [MaxLength(100)] + [EmailAddress] + public string Email { get; set; } = null!; + + [Required] + [MaxLength(10)] + public string Code { get; set; } = null!; + + [Required] public DateTime ExpirationDate { get; set; } + + public bool IsUsed { get; set; } = false; + + public bool IsExpired => DateTime.UtcNow > ExpirationDate; + + [Required] public OtpPurpose Purpose { get; set; } + + public int AttemptCount { get; set; } = 0; + + [MaxLength(45)] public string? IpAddress { get; set; } +} + +public enum OtpPurpose +{ + Login, + Registration, + PasswordReset, + EmailVerification +} \ No newline at end of file diff --git a/TaskGenix.Core/Domains/PaymentTransaction.cs b/TaskGenix.Core/Domains/PaymentTransaction.cs index c4c8f15..60be21b 100644 --- a/TaskGenix.Core/Domains/PaymentTransaction.cs +++ b/TaskGenix.Core/Domains/PaymentTransaction.cs @@ -10,41 +10,32 @@ namespace TaskGenix.Core.Domains [Table("PaymentTransactions")] public class PaymentTransaction : BaseEntity { - + [Key] [Required] - public decimal Amount { get; set; } + public Guid Id { get; set; } = Guid.NewGuid(); - [Required] - [MaxLength(3)] - public string Currency { get; set; } = "USD"; + [Required] public decimal Amount { get; set; } + + [Required][MaxLength(3)] public string Currency { get; set; } = "USD"; public DateTime TransactionDate { get; set; } = DateTime.UtcNow; // Payment method (Credit Card, PayPal, etc.) - [Required] - [MaxLength(50)] - public string PaymentMethod { get; set; } = null!; + [Required][MaxLength(50)] public string PaymentMethod { get; set; } = null!; // Payment gateway used (Stripe, PayPal, etc.) - [MaxLength(50)] - public string? PaymentGateway { get; set; } + [MaxLength(50)] public string? PaymentGateway { get; set; } // External transaction ID from payment processor - [MaxLength(100)] - public string? ExternalTransactionId { get; set; } + [MaxLength(100)] public string? ExternalTransactionId { get; set; } - [Required] - public PaymentStatus Status { get; set; } = PaymentStatus.Pending; + [Required] public PaymentStatus Status { get; set; } = PaymentStatus.Pending; - [ForeignKey("User")] - [Required] - public Guid UserId { get; set; } + [ForeignKey("User")][Required] public Guid UserId { get; set; } public User User { get; set; } = null!; - [ForeignKey("UserPlan")] - [Required] - public Guid UserPlanId { get; set; } + [ForeignKey("UserPlan")][Required] public Guid UserPlanId { get; set; } public UserPlan UserPlan { get; set; } = null!; } diff --git a/TaskGenix.Core/Domains/Plan.cs b/TaskGenix.Core/Domains/Plan.cs index 606296c..b99aee7 100644 --- a/TaskGenix.Core/Domains/Plan.cs +++ b/TaskGenix.Core/Domains/Plan.cs @@ -5,26 +5,28 @@ using System.Linq; using System.Threading.Tasks; -namespace TaskGenix.Core.Domains -{ - [Table("Plans")] - public class Plan : BaseEntity - { - - [Required, MaxLength(100)] - public string Name { get; set; } = null!; +namespace TaskGenix.Core.Domains; - [Required, MaxLength(500)] - public string Description { get; set; } = null!; +[Table("Plans")] +public class Plan : BaseEntity +{ + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); - public int DurationInDays { get; set; } - public decimal Price { get; set; } + [Required] + [MaxLength(100)] + public string Name { get; set; } = null!; + [Required] + [MaxLength(500)] + public string Description { get; set; } = null!; - public List Features { get; set; } = new List(); + public int DurationInDays { get; set; } + public decimal Price { get; set; } - public List UserPlans { get; set; } = new List(); + public List Features { get; set; } = new(); - } + public List UserPlans { get; set; } = new(); } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/PlanFeature.cs b/TaskGenix.Core/Domains/PlanFeature.cs index cbf3684..5766e72 100644 --- a/TaskGenix.Core/Domains/PlanFeature.cs +++ b/TaskGenix.Core/Domains/PlanFeature.cs @@ -7,6 +7,8 @@ namespace TaskGenix.Core.Domains [Table("PlanFeatures")] public class PlanFeature : BaseEntity { + [Key][Required] public Guid Id { get; set; } = Guid.NewGuid(); + // The quota value for this feature in this plan public decimal? QuotaValue { get; set; } @@ -15,14 +17,10 @@ public class PlanFeature : BaseEntity public bool IsUnlimited { get; set; } - [Required] - [ForeignKey("Plan")] - public Guid PlanId { get; set; } + [Required][ForeignKey("Plan")] public Guid PlanId { get; set; } public Plan Plan { get; set; } = null!; - [Required] - [ForeignKey("Feature")] - public Guid FeatureId { get; set; } + [Required][ForeignKey("Feature")] public Guid FeatureId { get; set; } public Feature Feature { get; set; } = null!; } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Domains/Project.cs b/TaskGenix.Core/Domains/Project.cs index 8446503..610372b 100644 --- a/TaskGenix.Core/Domains/Project.cs +++ b/TaskGenix.Core/Domains/Project.cs @@ -10,20 +10,15 @@ namespace TaskGenix.Core.Domains [Table("Projects")] public class Project : BaseEntity { + [Key][Required] public Guid Id { get; set; } = Guid.NewGuid(); - [Required] - [MaxLength(200)] - public string Name { get; set; } = null!; + [Required][MaxLength(200)] public string Name { get; set; } = null!; - [MaxLength(2000)] - public string Description { get; set; } = null!; + [MaxLength(2000)] public string Description { get; set; } = null!; - [Required] - public ProjectStatus Status { get; set; } - - [Required] - public ProjectType Type { get; set; } + [Required] public ProjectStatus Status { get; set; } + [Required] public ProjectType Type { get; set; } public Guid OwnerId { get; set; } public User Owner { get; set; } = null!; @@ -31,12 +26,13 @@ public class Project : BaseEntity public Guid? RecordingId { get; set; } public Recording? Recording { get; set; } - public List Members { get; set; } = new List(); + public List Members { get; set; } = new(); - public List TaskGens { get; set; } = new List(); + public List TaskGens { get; set; } = new(); public DateTime? DueDate { get; set; } + [Required] public bool IsActive { get; set; } = true; } diff --git a/TaskGenix.Core/Domains/ProjectMember.cs b/TaskGenix.Core/Domains/ProjectMember.cs index 69637bb..21c37bb 100644 --- a/TaskGenix.Core/Domains/ProjectMember.cs +++ b/TaskGenix.Core/Domains/ProjectMember.cs @@ -5,33 +5,31 @@ using System.Linq; using System.Threading.Tasks; -namespace TaskGenix.Core.Domains +namespace TaskGenix.Core.Domains; + +[Table("ProjectMembers")] +public class ProjectMember : BaseEntity { - [Table("ProjectMembers")] - public class ProjectMember : BaseEntity - { + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); - [Required] - public MemberRole Role { get; set; } + [Required] public MemberRole Role { get; set; } - [Required] - public Guid ProjectId { get; set; } - public Project Project { get; set; } = null!; + [Required] public Guid ProjectId { get; set; } + public Project Project { get; set; } = null!; - [Required] - public Guid UserId { get; set; } - public User User { get; set; } = null!; + [Required] public Guid UserId { get; set; } + public User User { get; set; } = null!; - [Required] - public DateTime JoinedAt { get; set; } = DateTime.UtcNow; + [Required] public DateTime JoinedAt { get; set; } = DateTime.UtcNow; +} - } - public enum MemberRole - { - Owner, - Member, - Viewer - } +public enum MemberRole +{ + Owner, + Member, + Viewer } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/Recording.cs b/TaskGenix.Core/Domains/Recording.cs index 5fc081d..eb8f5d5 100644 --- a/TaskGenix.Core/Domains/Recording.cs +++ b/TaskGenix.Core/Domains/Recording.cs @@ -5,33 +5,61 @@ using System.Linq; using System.Threading.Tasks; -namespace TaskGenix.Core.Domains +namespace TaskGenix.Core.Domains; + +[Table("Recordings")] +public class Recording : BaseEntity { - [Table("Recordings")] - public class Recording : BaseEntity - { + // Đề xuất: Loại bỏ = Guid.NewGuid() để DB PostgreSQL tự sinh UUID + [Key] + [Required] + public Guid Id { get; set; } + + [Required] + [MaxLength(200)] + public string Title { get; set; } = null!; // Tên hiển thị + + [MaxLength(255)] + public string? OriginalFileName { get; set; } // Tên file gốc người dùng upload - [Required] - [MaxLength(200)] - public string Title { get; set; } = null!; + [Required] + public string ObjectKey { get; set; } = null!; // Key chính trên R2 - [Required] - public string FileUrl { get; set; } = null!; + // Duration nên là TimeSpan? nếu có thể null + public TimeSpan? Duration { get; set; } - [Required] - public TimeSpan Duration { get; set; } + public long FileSize { get; set; } + [MaxLength(100)] + public string? MimeType { get; set; } - [Required] - [ForeignKey("Project")] - public Guid ProjectId { get; set; } - public Project Project { get; set; } = null!; + // Đề xuất sử dụng DateTimeOffset để đảm bảo xử lý múi giờ chính xác + public DateTimeOffset UploadedAt { get; set; } - [Required] - [ForeignKey("User")] - public Guid UserId { get; set; } - public User Owner { get; set; } = null!; + // --- Các trường Workflow STT --- + public bool IsTranscriptRequested { get; set; } + public TranscriptStatus TranscriptStatus { get; set; } = TranscriptStatus.None; + public string? TranscriptText { get; set; } + public string? TranscriptErrorMessage { get; set; } + // --- Khóa ngoại (Foreign Keys) --- + [ForeignKey("Project")] + public Guid? ProjectId { get; set; } + public Project? Project { get; set; } - } + // Chỉ sử dụng OwnerId để liên kết + [ForeignKey(nameof(Owner))] + public Guid? OwnerId { get; set; } + public User? Owner { get; set; } + + public string? OwnerEmail { get; set; } +} + +public enum TranscriptStatus +{ + None, + Pending, + Processing, + Success, + Failed } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/ResourceLink.cs b/TaskGenix.Core/Domains/ResourceLink.cs index 84efd22..dd2f39a 100644 --- a/TaskGenix.Core/Domains/ResourceLink.cs +++ b/TaskGenix.Core/Domains/ResourceLink.cs @@ -7,25 +7,19 @@ namespace TaskGenix.Core.Domains [Table("ResourceLinks")] public class ResourceLink : BaseEntity { + [Key][Required] public Guid Id { get; set; } = Guid.NewGuid(); - [Required] - [MaxLength(200)] - public string Title { get; set; } = null!; + [Required][MaxLength(200)] public string Title { get; set; } = null!; - [Required] - public string Url { get; set; } = null!; + [Required] public string Url { get; set; } = null!; public string? Description { get; set; } - [Required] - public ResourceType Type { get; set; } + [Required] public ResourceType Type { get; set; } - [ForeignKey("TaskGen")] - public Guid TaskGenId { get; set; } + [ForeignKey("TaskGen")] public Guid TaskGenId { get; set; } public TaskGen TaskGen { get; set; } = null!; - - } public enum ResourceType @@ -36,6 +30,9 @@ public enum ResourceType Tutorial, Blog, News, + Design, + Reference, + Tool, Other } } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/TaskAssignment.cs b/TaskGenix.Core/Domains/TaskAssignment.cs index 2872c83..926c1f8 100644 --- a/TaskGenix.Core/Domains/TaskAssignment.cs +++ b/TaskGenix.Core/Domains/TaskAssignment.cs @@ -2,26 +2,22 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace TaskGenix.Core.Domains +namespace TaskGenix.Core.Domains; + +[Table("TaskAssignments")] +public class TaskAssignment { - [Table("TaskAssignments")] - public class TaskAssignment - { - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); + [Key] public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] [ForeignKey("TaskGen")] public Guid TaskId { get; set; } + public TaskGen TaskGen { get; set; } = null!; - [Required] - [ForeignKey("TaskGen")] - public Guid TaskId { get; set; } - public TaskGen TaskGen { get; set; } = null!; + [Required] + [ForeignKey("AssignedUser")] + public Guid UserId { get; set; } - [Required] - [ForeignKey("AssignedUser")] - public Guid UserId { get; set; } - public User AssignedUser { get; set; } = null!; + public User AssignedUser { get; set; } = null!; - [Required] - public DateTime AssignedAt { get; set; } = DateTime.UtcNow; - } + [Required] public DateTime AssignedAt { get; set; } = DateTime.UtcNow; } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/TaskGen.cs b/TaskGenix.Core/Domains/TaskGen.cs index 54a405c..bce2979 100644 --- a/TaskGenix.Core/Domains/TaskGen.cs +++ b/TaskGenix.Core/Domains/TaskGen.cs @@ -10,33 +10,27 @@ namespace TaskGenix.Core.Domains [Table("TaskGens")] public class TaskGen : BaseEntity { + [Key][Required] public Guid Id { get; set; } = Guid.NewGuid(); - [Required] - [MaxLength(200)] - public string Title { get; set; } = null!; + [Required][MaxLength(200)] public string Title { get; set; } = null!; - [MaxLength(1000)] - public string Description { get; set; } = null!; + [MaxLength(1000)] public string Description { get; set; } = null!; - [Required] - public TaskGenStatus Status { get; set; } - [Required] - public TaskGenPriority Priority { get; set; } + [Required] public TaskGenStatus Status { get; set; } + [Required] public TaskGenPriority Priority { get; set; } - [ForeignKey("Project")] - public Guid ProjectId { get; set; } + [ForeignKey("Project")] public Guid ProjectId { get; set; } public Project Project { get; set; } = null!; - public List Attachments { get; set; } = new List(); - public List ResourceLinks { get; set; } = new List(); - public List Assignments { get; set; } = new List(); + public List Attachments { get; set; } = new(); + public List ResourceLinks { get; set; } = new(); + public List Assignments { get; set; } = new(); public TaskReminderConfig? ReminderConfig { get; set; } - public List Notifications { get; set; } = new List(); + public List Notifications { get; set; } = new(); - [Required] - public DateTime? DueDate { get; set; } + [Required] public DateTime? DueDate { get; set; } } public enum TaskGenStatus diff --git a/TaskGenix.Core/Domains/TaskReminderConfig.cs b/TaskGenix.Core/Domains/TaskReminderConfig.cs index 95873e8..99aac1c 100644 --- a/TaskGenix.Core/Domains/TaskReminderConfig.cs +++ b/TaskGenix.Core/Domains/TaskReminderConfig.cs @@ -10,10 +10,10 @@ namespace TaskGenix.Core.Domains [Table("TaskReminderConfigs")] public class TaskReminderConfig : BaseEntity { + [Key][Required] public Guid Id { get; set; } = Guid.NewGuid(); - [ForeignKey("TaskGen")] - [Required] - public Guid TaskGenId { get; set; } + + [ForeignKey("TaskGen")][Required] public Guid TaskGenId { get; set; } public TaskGen TaskGen { get; set; } = null!; @@ -29,8 +29,5 @@ public class TaskReminderConfig : BaseEntity public int PreferredReminderHour { get; set; } = 9; public bool EscalateReminderPriority { get; set; } = true; - - public DeliveryChannel PreferredChannel { get; set; } = DeliveryChannel.InApp; - } } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/User.cs b/TaskGenix.Core/Domains/User.cs index 225eb1b..3d7551e 100644 --- a/TaskGenix.Core/Domains/User.cs +++ b/TaskGenix.Core/Domains/User.cs @@ -1,56 +1,53 @@ -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using System.Threading.Tasks; namespace TaskGenix.Core.Domains { [Table("Users")] public class User : BaseEntity { + [Key][Required] public Guid Id { get; set; } = Guid.NewGuid(); - [Required] - [MaxLength(100)] - public string Username { get; set; } = null!; + [Required][MaxLength(100)] public string Username { get; set; } = null!; - public string? AvatarUrl { get; set; } + [MaxLength(150)] public string? AvatarUrl { get; set; } [Required] [MaxLength(100)] [EmailAddress] public string Email { get; set; } = null!; - [Required] - public string PasswordHash { get; set; } = null!; + [Required] public string PasswordHash { get; set; } = null!; - [Required] - public UserRole Role { get; set; } + [Required] public UserRole Role { get; set; } public DateTime? LastLogin { get; set; } public bool EmailVerified { get; set; } - [Required] - public bool IsActive { get; set; } + public string? Auth0Id { get; set; } + + [Required] public bool IsActive { get; set; } public UserSetting? Settings { get; set; } - public List ProjectMemberships { get; set; } = new List(); + public List ProjectMemberships { get; set; } = new(); + + public List TaskAssignments { get; set; } = new(); - public List TaskAssignments { get; set; } = new List(); + public List OwnedProjects { get; set; } = new(); - public List OwnedProjects { get; set; } = new List(); + public List DeviceTokens { get; set; } = new(); - public List DeviceTokens { get; set; } = new List(); + public List UserPlans { get; set; } = new(); - public List UserPlans { get; set; } = new List(); + public List Notifications { get; set; } = new(); - public List Notifications { get; set; } = new List(); + public List PaymentTransactions { get; set; } = new(); - public List PaymentTransactions { get; set; } = new List(); + public List JwtTokens { get; set; } = new(); + public List Recordings { get; set; } = new(); } public enum UserRole diff --git a/TaskGenix.Core/Domains/UserDeviceToken.cs b/TaskGenix.Core/Domains/UserDeviceToken.cs index 0cf3b3a..48750ea 100644 --- a/TaskGenix.Core/Domains/UserDeviceToken.cs +++ b/TaskGenix.Core/Domains/UserDeviceToken.cs @@ -10,9 +10,14 @@ namespace TaskGenix.Core.Domains [Table("UserDeviceTokens")] public class UserDeviceToken : BaseEntity { + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); + [Required] public string FCMToken { get; set; } = null!; + [Required] public string DeviceId { get; set; } = null!; @@ -22,12 +27,8 @@ public class UserDeviceToken : BaseEntity [Required] public string Platform { get; set; } = null!; - - [ForeignKey("User")] [Required] - public Guid UserId { get; set; } - public User User { get; set; } = null!; - - + [EmailAddress] + public string Email { get; set; } = null!; } } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/UserPlan.cs b/TaskGenix.Core/Domains/UserPlan.cs index 93a7320..6dd3a75 100644 --- a/TaskGenix.Core/Domains/UserPlan.cs +++ b/TaskGenix.Core/Domains/UserPlan.cs @@ -10,6 +10,10 @@ namespace TaskGenix.Core.Domains [Table("UserPlans")] public class UserPlan : BaseEntity { + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } = null!; @@ -26,7 +30,7 @@ public class UserPlan : BaseEntity public Guid PlanId { get; set; } public Plan Plan { get; set; } = null!; - public List Features { get; set; } = new List(); + public List Features { get; set; } = new(); [Required] @@ -36,6 +40,5 @@ public class UserPlan : BaseEntity public DateTime EndDate { get; set; } public bool IsActive { get; set; } - } } \ No newline at end of file diff --git a/TaskGenix.Core/Domains/UserPlanFeature.cs b/TaskGenix.Core/Domains/UserPlanFeature.cs index cb2be93..8bd020e 100644 --- a/TaskGenix.Core/Domains/UserPlanFeature.cs +++ b/TaskGenix.Core/Domains/UserPlanFeature.cs @@ -7,17 +7,22 @@ namespace TaskGenix.Core.Domains [Table("UserPlanFeatures")] public class UserPlanFeature : BaseEntity { + [Key] [Required] - public decimal QuotaTotal { get; set; } + public Guid Id { get; set; } = Guid.NewGuid(); - [Required] - public decimal QuotaUsed { get; set; } + + public decimal? QuotaTotal { get; set; } + + public decimal? QuotaUsed { get; set; } // Remaining quota (could be calculated, but stored for query efficiency) - public decimal QuotaRemaining { get; set; } + public decimal? QuotaRemaining { get; set; } // Whether this feature is unlimited for this user's plan public bool IsUnlimited { get; set; } + [Required] + public string? FeatureKey { get; set; } [Required] @@ -26,9 +31,25 @@ public class UserPlanFeature : BaseEntity public UserPlan UserPlan { get; set; } = null!; [Required] - [ForeignKey("Feature")] - public Guid FeatureId { get; set; } - public Feature Feature { get; set; } = null!; - + [ForeignKey("PlanFeature")] + public Guid PlanFeatureId { get; set; } + public PlanFeature PlanFeature { get; set; } = null!; + public void RecalculateQuotaRemaining() + { + if (IsUnlimited) + { + QuotaRemaining = null; + QuotaTotal = null; + QuotaUsed = null; + return; + } + var remaining = QuotaTotal; + if (QuotaRemaining == QuotaTotal) + { + remaining = QuotaTotal - QuotaUsed; + } + else remaining = QuotaRemaining - QuotaUsed; + QuotaRemaining = remaining < 0 ? 0m : remaining; + } } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Domains/UserSetting.cs b/TaskGenix.Core/Domains/UserSetting.cs index 6847a12..6db9473 100644 --- a/TaskGenix.Core/Domains/UserSetting.cs +++ b/TaskGenix.Core/Domains/UserSetting.cs @@ -10,6 +10,11 @@ namespace TaskGenix.Core.Domains [Table("UserSettings")] public class UserSetting : BaseEntity { + [Key] + [Required] + public Guid Id { get; set; } = Guid.NewGuid(); + + public bool ReceiveEmailNotifications { get; set; } public bool DarkModeEnabled { get; set; } diff --git a/TaskGenix.Core/Exceptions/ApiException.cs b/TaskGenix.Core/Exceptions/ApiException.cs index be1da0e..d89cbc5 100644 --- a/TaskGenix.Core/Exceptions/ApiException.cs +++ b/TaskGenix.Core/Exceptions/ApiException.cs @@ -1,86 +1,86 @@ using System.Net; -namespace TaskGenix.Core.Exceptions +namespace TaskGenix.Core.Exceptions; + +public class ApiException : Exception { - public class ApiException : Exception - { - public HttpStatusCode StatusCode { get; } - public string Reason { get; } + public HttpStatusCode StatusCode { get; } + public string Reason { get; } - public ApiException(string reason, HttpStatusCode statusCode = HttpStatusCode.InternalServerError) - : base(reason) - { - StatusCode = statusCode; - Reason = reason; - } + public ApiException(string reason, HttpStatusCode statusCode = HttpStatusCode.InternalServerError) + : base(reason) + { + StatusCode = statusCode; + Reason = reason; } +} - public class NotFoundException : ApiException +public class NotFoundException : ApiException +{ + public NotFoundException(string reason) + : base(reason, HttpStatusCode.NotFound) { - public NotFoundException(string reason) - : base(reason, HttpStatusCode.NotFound) - { - } } +} - public class BadRequestException : ApiException +public class BadRequestException : ApiException +{ + public BadRequestException(string reason) + : base(reason, HttpStatusCode.BadRequest) { - public BadRequestException(string reason) - : base(reason, HttpStatusCode.BadRequest) - { - } } +} - public class UnauthorizedException : ApiException +public class UnauthorizedException : ApiException +{ + public UnauthorizedException(string reason) + : base(reason, HttpStatusCode.Unauthorized) { - public UnauthorizedException(string reason) - : base(reason, HttpStatusCode.Unauthorized) - { - } } +} - public class BusinessException : ApiException +public class BusinessException : ApiException +{ + public BusinessException(string reason) + : base(reason, HttpStatusCode.BadRequest) { - public BusinessException(string reason) - : base(reason, HttpStatusCode.BadRequest) - { - } } +} - public class ValidationException : ApiException +public class ValidationException : ApiException +{ + public List Errors { get; } + + public ValidationException(string reason, List? errors = null) + : base(reason, HttpStatusCode.UnprocessableEntity) { - public List Errors { get; } - public ValidationException(string reason, List? errors = null) - : base(reason, HttpStatusCode.UnprocessableEntity) - { - Errors = errors ?? new List(); - } + Errors = errors ?? new List(); } +} - public class ForbiddenException : ApiException +public class ForbiddenException : ApiException +{ + public ForbiddenException(string reason) + : base(reason, HttpStatusCode.Forbidden) { - public ForbiddenException(string reason) - : base(reason, HttpStatusCode.Forbidden) - { - } } +} - public class ConflictException : ApiException +public class ConflictException : ApiException +{ + public ConflictException(string reason) + : base(reason, HttpStatusCode.Conflict) { - public ConflictException(string reason) - : base(reason, HttpStatusCode.Conflict) - { - } } +} - public class TooManyRequestsException : ApiException - { - public DateTime? RetryAfter { get; } +public class TooManyRequestsException : ApiException +{ + public DateTime? RetryAfter { get; } - public TooManyRequestsException(string reason, DateTime? retryAfter = null) - : base(reason, HttpStatusCode.TooManyRequests) - { - RetryAfter = retryAfter; - } + public TooManyRequestsException(string reason, DateTime? retryAfter = null) + : base(reason, HttpStatusCode.TooManyRequests) + { + RetryAfter = retryAfter; } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Extensions/ApplyMigration.cs b/TaskGenix.Core/Extensions/ApplyMigration.cs index 92fedf4..581fa3d 100644 --- a/TaskGenix.Core/Extensions/ApplyMigration.cs +++ b/TaskGenix.Core/Extensions/ApplyMigration.cs @@ -5,44 +5,41 @@ using System; using System.Threading; -namespace TaskGenix.Core.Extensions +namespace TaskGenix.Core.Extensions; + +public static class ApplyMigration { - public static class ApplyMigration + public static void ApplyMigrations(this IApplicationBuilder app) where TContext : DbContext { - public static void ApplyMigrations(this IApplicationBuilder app) where TContext : DbContext - { - using IServiceScope scope = app.ApplicationServices.CreateScope(); - using TContext context = scope.ServiceProvider.GetRequiredService(); + using var scope = app.ApplicationServices.CreateScope(); + using var context = scope.ServiceProvider.GetRequiredService(); - try - { - var retryCount = 0; - const int maxRetries = 5; + try + { + var retryCount = 0; + const int maxRetries = 5; - while (retryCount < maxRetries) + while (retryCount < maxRetries) + try { - try - { - context.Database.Migrate(); - break; - } - catch (Exception) - { - retryCount++; - if (retryCount == maxRetries) - throw; + context.Database.Migrate(); + break; + } + catch (Exception) + { + retryCount++; + if (retryCount == maxRetries) + throw; - // Wait before the next retry - Thread.Sleep(2000 * retryCount); // Exponential backoff - } + // Wait before the next retry + Thread.Sleep(2000 * retryCount); // Exponential backoff } - } - catch (Exception ex) - { - var logger = scope.ServiceProvider.GetRequiredService>(); - logger.LogError(ex, "An error occurred while migrating the database."); - throw; - } + } + catch (Exception ex) + { + var logger = scope.ServiceProvider.GetRequiredService>(); + logger.LogError(ex, "An error occurred while migrating the database."); + throw; } } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Mappings/MapsterConfig.cs b/TaskGenix.Core/Mappings/MapsterConfig.cs index 7b0a7f7..02ef80b 100644 --- a/TaskGenix.Core/Mappings/MapsterConfig.cs +++ b/TaskGenix.Core/Mappings/MapsterConfig.cs @@ -2,15 +2,14 @@ using System; using System.Reflection; -namespace TaskGenix.Core.Mappings +namespace TaskGenix.Core.Mappings; + +public static class MapsterConfig { - public static class MapsterConfig + public static void RegisterMappings() { - public static void RegisterMappings() - { - // Scan and register all mapping configurations in the assembly - // This will automatically find all classes implementing IRegister - TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly()); - } + // Scan and register all mapping configurations in the assembly + // This will automatically find all classes implementing IRegister + TypeAdapterConfig.GlobalSettings.Scan(Assembly.GetExecutingAssembly()); } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Mappings/NotificationMappingRegister.cs b/TaskGenix.Core/Mappings/NotificationMappingRegister.cs new file mode 100644 index 0000000..ee7b0a0 --- /dev/null +++ b/TaskGenix.Core/Mappings/NotificationMappingRegister.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Mapster; +using TaskGenix.Core.Responses.Notification; + +namespace TaskGenix.Core.Mappings; + +public class NotificationMappingRegister : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig(); + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Mappings/ProjectMappingRegister.cs b/TaskGenix.Core/Mappings/ProjectMappingRegister.cs index 55efaaa..ab5a3c0 100644 --- a/TaskGenix.Core/Mappings/ProjectMappingRegister.cs +++ b/TaskGenix.Core/Mappings/ProjectMappingRegister.cs @@ -5,18 +5,36 @@ using System.Text; using System.Threading.Tasks; using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.Project; using TaskGenix.Core.Responses.Project; -namespace TaskGenix.Core.Mappings -{ - public class ProjectMappingRegister : IRegister +namespace TaskGenix.Core.Mappings; + +public class ProjectMappingRegister : IRegister +{ + public void Register(TypeAdapterConfig config) { - public void Register(TypeAdapterConfig config) - { - // Project -> ProjectResponse - config.NewConfig() - .Map(dest => dest.Members, src => src.Members); - } + // Project -> ProjectResponse + config.NewConfig() + .Map(dest => dest.Members, src => src.Members) + .Map(dest => dest.TaskGens, src => src.TaskGens); + + // CreateProjectRequest -> Project + config.NewConfig() + .Map(dest => dest.Name, src => src.Name) + .Map(dest => dest.Description, src => src.Description) + .Map(dest => dest.Status, src => src.Status) + .Map(dest => dest.Type, src => src.Type) + .Map(dest => dest.OwnerId, src => src.OwnerId) + .Map(dest => dest.DueDate, src => src.DueDate); + + // UpdateProjectRequest -> Project + config.NewConfig() + .Map(dest => dest.Name, src => src.Name) + .Map(dest => dest.Description, src => src.Description) + .Map(dest => dest.Status, src => src.Status) + .Map(dest => dest.Type, src => src.Type) + .Map(dest => dest.DueDate, src => src.DueDate); } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Mappings/ProjectMemberMappingRegister.cs b/TaskGenix.Core/Mappings/ProjectMemberMappingRegister.cs new file mode 100644 index 0000000..0b8e8f1 --- /dev/null +++ b/TaskGenix.Core/Mappings/ProjectMemberMappingRegister.cs @@ -0,0 +1,19 @@ +using Mapster; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Responses.Project; + +namespace TaskGenix.Core.Mappings; + +public class ProjectMemberMappingRegister : IRegister +{ + public void Register(TypeAdapterConfig config) + { + // ProjectMember -> ProjectMemberResponse + config.NewConfig() + .Map(dest => dest.User, src => src.User) + .Map(dest => dest.Role, src => src.Role) + .Map(dest => dest.JoinedAt, src => src.JoinedAt) + .Map(dest => dest.CreatedAt, src => src.CreatedAt) + .Map(dest => dest.UpdatedAt, src => src.UpdatedAt); + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Mappings/TaskMappingRegister.cs b/TaskGenix.Core/Mappings/TaskMappingRegister.cs index 9052673..4d12002 100644 --- a/TaskGenix.Core/Mappings/TaskMappingRegister.cs +++ b/TaskGenix.Core/Mappings/TaskMappingRegister.cs @@ -3,32 +3,35 @@ using TaskGenix.Core.Requests.TaskGen; using TaskGenix.Core.Responses.TaskGen; -namespace TaskGenix.Core.Mappings +namespace TaskGenix.Core.Mappings; + +public class TaskMappingRegister : IRegister { - public class TaskMappingRegister : IRegister + public void Register(TypeAdapterConfig config) { - public void Register(TypeAdapterConfig config) - { - // TaskGen -> TaskGenResponse - config.NewConfig() - .Map(dest => dest.Assignments, src => src.Assignments) - .Map(dest => dest.Status, src => src.Status.ToString()); + // TaskGen -> TaskGenResponse + config.NewConfig() + .Map(dest => dest.Assignments, src => src.Assignments) + .Map(dest => dest.Resources, src => src.ResourceLinks) + .Map(dest => dest.Status, src => src.Status.ToString()); - // TaskAssignment -> TaskAssignmentResponse - config.NewConfig() - .Map(dest => dest.AssignedUser, src => src.AssignedUser) - .Map(dest => dest.AssignedAt, src => src.AssignedAt); + // TaskAssignment -> TaskAssignmentResponse + config.NewConfig() + .Map(dest => dest.AssignedUser, src => src.AssignedUser) + .Map(dest => dest.AssignedAt, src => src.AssignedAt); + // ResourceLink -> ResourceResponse + config.NewConfig() + .Map(dest => dest.Type, src => src.Type.ToString()); - // CreateTaskRequest -> TaskGen - config.NewConfig() - .Ignore(dest => dest.Assignments); + // CreateTaskRequest -> TaskGen + config.NewConfig() + .Ignore(dest => dest.Assignments); - // UpdateTaskRequest -> TaskGen - config.NewConfig() - .IgnoreNullValues(true) - .Ignore(dest => dest.ProjectId) - .Ignore(dest => dest.Assignments); - } + // UpdateTaskRequest -> TaskGen + config.NewConfig() + .IgnoreNullValues(true) + .Ignore(dest => dest.ProjectId) + .Ignore(dest => dest.Assignments); } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Mappings/UserMappingRegister.cs b/TaskGenix.Core/Mappings/UserMappingRegister.cs index d190549..331a05d 100644 --- a/TaskGenix.Core/Mappings/UserMappingRegister.cs +++ b/TaskGenix.Core/Mappings/UserMappingRegister.cs @@ -2,14 +2,40 @@ using TaskGenix.Core.Domains; using TaskGenix.Core.Responses.User; -namespace TaskGenix.Core.Mappings +namespace TaskGenix.Core.Mappings; + +public class UserMappingRegister : IRegister { - public class UserMappingRegister : IRegister + public void Register(TypeAdapterConfig config) { - public void Register(TypeAdapterConfig config) - { - // User -> UserResponse - config.NewConfig(); - } + // User -> UserResponse + config.NewConfig() + .Map(dest => dest.Id, src => src.Id) + .Map(dest => dest.Username, src => src.Username) + .Map(dest => dest.Email, src => src.Email) + .Map(dest => dest.ProfilePicture, src => src.AvatarUrl); + + // UserRequest -> User (for creation/update scenarios) + config.NewConfig() + .Map(dest => dest.Username, src => src.Username) + .Map(dest => dest.Email, src => src.Email) + .Ignore(dest => dest.Id) // ID should be generated + .Ignore(dest => dest.PasswordHash) // Handle password hashing separately + .Ignore(dest => dest.Role) // Set default role in service + .Ignore(dest => dest.IsActive) // Set default in service + .Ignore(dest => dest.EmailVerified) // Set default in service + .Ignore(dest => dest.CreatedAt) // Set in service + .Ignore(dest => dest.UpdatedAt) // Set in service + .Ignore(dest => dest.LastLogin) + .Ignore(dest => dest.AvatarUrl) + .Ignore(dest => dest.Settings) + .Ignore(dest => dest.ProjectMemberships) + .Ignore(dest => dest.TaskAssignments) + .Ignore(dest => dest.OwnedProjects) + .Ignore(dest => dest.DeviceTokens) + .Ignore(dest => dest.UserPlans) + .Ignore(dest => dest.Notifications) + .Ignore(dest => dest.PaymentTransactions) + .Ignore(dest => dest.JwtTokens); } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Mappings/UserPlanFeatureMappingRegister.cs b/TaskGenix.Core/Mappings/UserPlanFeatureMappingRegister.cs new file mode 100644 index 0000000..f316264 --- /dev/null +++ b/TaskGenix.Core/Mappings/UserPlanFeatureMappingRegister.cs @@ -0,0 +1,33 @@ +using Mapster; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Responses.UserPlanFeature; + +namespace TaskGenix.Core.Mappings +{ + public class UserPlanFeatureMappingRegister : IRegister + { + public void Register(TypeAdapterConfig config) + { + // UserPlanFeature -> CreateUserPlanFeatureResponse (flatten PlanFeature -> Feature) + config.NewConfig() + .Map(dest => dest.Id, src => src.Id) + .Map(dest => dest.UserPlanId, src => src.UserPlanId) + .Map(dest => dest.PlanFeatureId, src => src.PlanFeatureId) + .Map(dest => dest.QuotaTotal, src => src.QuotaTotal) + .Map(dest => dest.QuotaUsed, src => src.QuotaUsed) + .Map(dest => dest.QuotaRemaining, src => src.QuotaRemaining) + .Map(dest => dest.IsUnlimited, src => src.IsUnlimited) + .Map(dest => dest.PlanFeatureQuotaValue, src => src.PlanFeature != null ? src.PlanFeature.QuotaValue : null) + .Map(dest => dest.FeatureId, src => src.PlanFeature != null && src.PlanFeature.Feature != null ? src.PlanFeature.Feature.Id : (Guid?)null) + .Map(dest => dest.FeatureKey, src => src.PlanFeature != null && src.PlanFeature.Feature != null ? src.PlanFeature.Feature.Key : null) + .Map(dest => dest.FeatureName, src => src.PlanFeature != null && src.PlanFeature.Feature != null ? src.PlanFeature.Feature.Name : null) + .Map(dest => dest.FeatureUnit, src => src.PlanFeature != null && src.PlanFeature.Feature != null ? src.PlanFeature.Feature.Unit : null) + .Map(dest => dest.FeatureType, src => src.PlanFeature != null && src.PlanFeature.Feature != null ? src.PlanFeature.Feature.FeatureType.ToString() : null); + } + } +} diff --git a/TaskGenix.Core/Mappings/UserPlanMappingRegister.cs b/TaskGenix.Core/Mappings/UserPlanMappingRegister.cs new file mode 100644 index 0000000..2202193 --- /dev/null +++ b/TaskGenix.Core/Mappings/UserPlanMappingRegister.cs @@ -0,0 +1,47 @@ +using Mapster; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.UserPlan; +using TaskGenix.Core.Responses.UserPlan; + +namespace TaskGenix.Core.Mappings +{ + public class UserPlanMappingRegister : IRegister + { + public void Register(TypeAdapterConfig config) + { + // UserPlan -> UserPlanResponse + config.NewConfig() + .Map(dest => dest.Id, src => src.Id) + .Map(dest => dest.Name, src => src.Name) + .Map(dest => dest.Price, src => src.Price) + .Map(dest => dest.UserId, src => src.UserId) + .Map(dest => dest.PlanId, src => src.PlanId) + .Map(dest => dest.StartDate, src => src.StartDate) + .Map(dest => dest.EndDate, src => src.EndDate) + .Map(dest => dest.IsActive, src => src.IsActive) + .Map(dest => dest.CreatedAt, src => src.CreatedAt) + .Map(dest => dest.UpdatedAt, src => src.UpdatedAt); + + // UserPlanRequest -> UserPlan + config.NewConfig() + .Map(dest => dest.Name, src => src.Name) + .Map(dest => dest.Price, src => src.Price) + .Map(dest => dest.UserId, src => src.UserId) + .Map(dest => dest.PlanId, src => src.PlanId) + .Map(dest => dest.StartDate, src => src.StartDate) + .Map(dest => dest.EndDate, src => src.EndDate) + .Map(dest => dest.IsActive, src => src.IsActive); + + // UpdateUserPlanRequest -> UserPlan + config.NewConfig() + .IgnoreNullValues(true) + .Map(dest => dest.Name, src => src.Name) + .Map(dest => dest.Price, src => src.Price) + .Map(dest => dest.UserId, src => src.UserId) + .Map(dest => dest.PlanId, src => src.PlanId) + .Map(dest => dest.StartDate, src => src.StartDate) + .Map(dest => dest.EndDate, src => src.EndDate) + .Map(dest => dest.IsActive, src => src.IsActive); + } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Meta/ApiResponse.cs b/TaskGenix.Core/Meta/ApiResponse.cs index 07d8bea..0c1b527 100644 --- a/TaskGenix.Core/Meta/ApiResponse.cs +++ b/TaskGenix.Core/Meta/ApiResponse.cs @@ -1,26 +1,25 @@ using System.Text.Json.Serialization; -namespace TaskGenix.Core.Meta -{ - public class ApiResponse - { - [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; +namespace TaskGenix.Core.Meta; - [JsonPropertyName("data")] public T? Data { get; set; } - } +public class ApiResponse +{ + [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; - // Success response class - simple with just data and message - public class SuccessResponse - { - [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; - [JsonPropertyName("data")] public T? Data { get; set; } - } + [JsonPropertyName("data")] public T? Data { get; set; } +} - // Error response class - more detailed error information - public class ErrorResponse - { - [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; - [JsonPropertyName("reason")] public string? Reason { get; set; } - [JsonPropertyName("errors")] public List Errors { get; set; } = new List(); - } +public class SuccessResponse +{ + [JsonPropertyName("success")] public bool Success { get; set; } = true; + [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; + [JsonPropertyName("data")] public T? Data { get; set; } } + +// Error response class - more detailed error information +public class ErrorResponse +{ + [JsonPropertyName("message")] public string Message { get; set; } = string.Empty; + [JsonPropertyName("reason")] public string? Reason { get; set; } + [JsonPropertyName("errors")] public List Errors { get; set; } = new(); +} \ No newline at end of file diff --git a/TaskGenix.Core/Meta/ApiResponseBuilder.cs b/TaskGenix.Core/Meta/ApiResponseBuilder.cs index e171b14..6dc7f26 100644 --- a/TaskGenix.Core/Meta/ApiResponseBuilder.cs +++ b/TaskGenix.Core/Meta/ApiResponseBuilder.cs @@ -1,58 +1,69 @@ -namespace TaskGenix.Core.Meta +using TaskGenix.Core.Constants; + +namespace TaskGenix.Core.Meta; + +public class ApiResponseBuilder { - public class ApiResponseBuilder + // Success response - without data + public static SuccessResponse BuildSuccessResponse() { - // Success response - simpler format with just message and data - public static SuccessResponse BuildSuccessResponse(T? data, string message = "Operation completed successfully") + return new SuccessResponse { - return new SuccessResponse - { - Message = message, - Data = data - }; - } + Message = CommonMessage.OperationSuccessful, + Data = null + }; + } - // Error response - with more detailed error information - public static ErrorResponse BuildErrorResponse( - string message = "An error occurred", - string? reason = null, - List? errors = null) + // Success response - simpler format with just message and data + public static SuccessResponse BuildSuccessResponse(T? data, + string message = CommonMessage.OperationSuccessful) + { + return new SuccessResponse { - return new ErrorResponse - { - Message = message, - Reason = reason, - Errors = errors ?? new List() - }; - } + Message = message, + Data = data + }; + } - // For pagination, we'll use the success format - public static SuccessResponse> BuildPageResponse( - IEnumerable items, - int totalPages, - int currentPage, - int pageSize, - long totalItems, - string message = "Data retrieved successfully") + // Error response - with more detailed error information + public static ErrorResponse BuildErrorResponse( + string message = "An error occurred", + string? reason = null, + List? errors = null) + { + return new ErrorResponse { - var pagedData = new PagedData - { - Items = items, - Meta = new PaginationMeta() - { - TotalPages = totalPages, - CurrentPage = currentPage, - PageSize = pageSize, - TotalItems = totalItems - } - }; + Message = message, + Reason = reason, + Errors = errors ?? new List() + }; + } - return new SuccessResponse> + // For pagination, we'll use the success format + public static SuccessResponse> BuildPageResponse( + IEnumerable items, + int totalPages, + int currentPage, + int pageSize, + long totalItems, + string message = "Data retrieved successfully") + { + var pagedData = new PagedData + { + Items = items, + Meta = new PaginationMeta { - Message = message, - Data = pagedData - }; - } - } -} + TotalPages = totalPages, + CurrentPage = currentPage, + PageSize = pageSize, + TotalItems = totalItems + } + }; + return new SuccessResponse> + { + Message = message, + Data = pagedData + }; + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Meta/PagedData.cs b/TaskGenix.Core/Meta/PagedData.cs index f7b19e7..87d23d1 100644 --- a/TaskGenix.Core/Meta/PagedData.cs +++ b/TaskGenix.Core/Meta/PagedData.cs @@ -1,13 +1,10 @@ using System.Text.Json.Serialization; -namespace TaskGenix.Core.Meta +namespace TaskGenix.Core.Meta; + +public class PagedData { - public class PagedData - { - [JsonPropertyName("items")] - public required IEnumerable Items { get; set; } + [JsonPropertyName("items")] public required IEnumerable Items { get; set; } - [JsonPropertyName("meta")] - public required PaginationMeta Meta { get; set; } - } -} + [JsonPropertyName("meta")] public required PaginationMeta Meta { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Meta/PaginateExtentions.cs b/TaskGenix.Core/Meta/PaginateExtentions.cs index 4ddbf5b..2536ce6 100644 --- a/TaskGenix.Core/Meta/PaginateExtentions.cs +++ b/TaskGenix.Core/Meta/PaginateExtentions.cs @@ -1,30 +1,30 @@ using Microsoft.EntityFrameworkCore; -namespace TaskGenix.Core.Meta +namespace TaskGenix.Core.Meta; + +public static class PaginateExtensions { - public static class PaginateExtensions + public static async Task> ToPaginatedResponse(this IQueryable query, int pageNumber, + int pageSize, int firstPage = 1) { - public static async Task> ToPaginatedResponse(this IQueryable query, int pageNumber, int pageSize, int firstPage = 1) - { - if (firstPage > pageNumber) - throw new ArgumentException($"page ({pageNumber}) must greater or equal than firstPage ({firstPage})"); + if (firstPage > pageNumber) + throw new ArgumentException($"page ({pageNumber}) must greater or equal than firstPage ({firstPage})"); - var totalItems = await query.CountAsync(); - var items = await query.Skip((pageNumber - 1) * pageSize) - .Take(pageSize) - .ToListAsync(); + var totalItems = await query.CountAsync(); + var items = await query.Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + .ToListAsync(); - return new PagedData + return new PagedData + { + Items = items, + Meta = new PaginationMeta { - Items = items, - Meta = new PaginationMeta - { - CurrentPage = pageNumber, - PageSize = pageSize, - TotalItems = totalItems, - TotalPages = (int)Math.Ceiling(totalItems / (double)pageSize) - } - }; - } + CurrentPage = pageNumber, + PageSize = pageSize, + TotalItems = totalItems, + TotalPages = (int)Math.Ceiling(totalItems / (double)pageSize) + } + }; } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Meta/PaginationMeta.cs b/TaskGenix.Core/Meta/PaginationMeta.cs index eb87896..97ebf25 100644 --- a/TaskGenix.Core/Meta/PaginationMeta.cs +++ b/TaskGenix.Core/Meta/PaginationMeta.cs @@ -1,19 +1,14 @@ using System.Text.Json.Serialization; -namespace TaskGenix.Core.Meta +namespace TaskGenix.Core.Meta; + +public class PaginationMeta { - public class PaginationMeta - { - [JsonPropertyName("total_pages")] - public int TotalPages { get; set; } + [JsonPropertyName("total_pages")] public int TotalPages { get; set; } - [JsonPropertyName("total_items")] - public long TotalItems { get; set; } + [JsonPropertyName("total_items")] public long TotalItems { get; set; } - [JsonPropertyName("current_page")] - public int CurrentPage { get; set; } + [JsonPropertyName("current_page")] public int CurrentPage { get; set; } - [JsonPropertyName("page_size")] - public int PageSize { get; set; } - } -} + [JsonPropertyName("page_size")] public int PageSize { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Middlewares/ApiExceptionHandlerMiddleware.cs b/TaskGenix.Core/Middlewares/ApiExceptionHandlerMiddleware.cs index 34201ab..10e7ed1 100644 --- a/TaskGenix.Core/Middlewares/ApiExceptionHandlerMiddleware.cs +++ b/TaskGenix.Core/Middlewares/ApiExceptionHandlerMiddleware.cs @@ -5,181 +5,175 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace TaskGenix.Core.Middlewares +namespace TaskGenix.Core.Middlewares; + +public class ApiExceptionHandlerMiddleware { - public class ApiExceptionHandlerMiddleware + // Fields + private readonly ILogger _logger; + private readonly RequestDelegate _next; + private readonly IHostEnvironment _env; + + //constructor + public ApiExceptionHandlerMiddleware( + RequestDelegate next, + ILogger logger, + IHostEnvironment environment) { - // Fields - private readonly ILogger _logger; - private readonly RequestDelegate _next; - private readonly IHostEnvironment _env; - - //constructor - public ApiExceptionHandlerMiddleware( - RequestDelegate next, - ILogger logger, - IHostEnvironment environment) - { - _next = next; - _logger = logger; - _env = environment; - } + _next = next; + _logger = logger; + _env = environment; + } - public async Task InvokeAsync(HttpContext context) + public async Task InvokeAsync(HttpContext context) + { + var requestStartTime = DateTime.UtcNow; + var requestId = context.TraceIdentifier; + + try { - var requestStartTime = DateTime.UtcNow; - var requestId = context.TraceIdentifier; - - try - { - // Initial request logging - _logger.LogInformation( - "ApiExceptionMiddleware: Starting request processing - RequestId: {RequestId}, Path: {Path}, Method: {Method}", - requestId, - context.Request.Path, - context.Request.Method); - - // Check API Gateway - bool isFromGateway = context.Request.Headers.TryGetValue("Api-Gateway", out var gatewayHeader); - _logger.LogInformation( - "Request source: {Source}", - isFromGateway ? "API Gateway" : "Direct Client"); - - // Authentication status - var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false; - _logger.LogInformation( - "Authentication Status - IsAuthenticated: {IsAuthenticated}, Identity: {IdentityName}", - isAuthenticated, - context.User?.Identity?.Name ?? "none"); - - await _next(context); - - // Log response status - var requestDuration = DateTime.UtcNow - requestStartTime; - _logger.LogInformation( - "Request completed - RequestId: {RequestId}, StatusCode: {StatusCode}, Duration: {Duration}ms", - requestId, - context.Response.StatusCode, - requestDuration.TotalMilliseconds); - } - catch (Exception exception) - { - var errorId = Guid.NewGuid().ToString(); - _logger.LogError( - exception, - "Unhandled exception - RequestId: {RequestId}, ErrorId: {ErrorId}, Message: {Message}", - requestId, - errorId, - exception.Message); - - LogError(errorId, context, exception); - await HandleExceptionAsync(context, errorId, exception); - } + // Initial request logging + _logger.LogInformation( + "ApiExceptionMiddleware: Starting request processing - RequestId: {RequestId}, Path: {Path}, Method: {Method}", + requestId, + context.Request.Path, + context.Request.Method); + + // Check API Gateway + var isFromGateway = context.Request.Headers.TryGetValue("Api-Gateway", out var gatewayHeader); + _logger.LogInformation( + "Request source: {Source}", + isFromGateway ? "API Gateway" : "Direct Client"); + + // Authentication status + var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false; + _logger.LogInformation( + "Authentication Status - IsAuthenticated: {IsAuthenticated}, Identity: {IdentityName}", + isAuthenticated, + context.User?.Identity?.Name ?? "none"); + + await _next(context); + + // Log response status + var requestDuration = DateTime.UtcNow - requestStartTime; + _logger.LogInformation( + "Request completed - RequestId: {RequestId}, StatusCode: {StatusCode}, Duration: {Duration}ms", + requestId, + context.Response.StatusCode, + requestDuration.TotalMilliseconds); } - - private void LogError(string errorId, HttpContext context, Exception exception) + catch (Exception exception) { - var error = new - { - ErrorId = errorId, - Timestamp = DateTime.UtcNow, - RequestPath = context.Request.Path, - RequestMethod = context.Request.Method, - ExceptionType = exception.GetType().Name, - ExceptionMessage = exception.Message, - }; - - var logLevel = exception switch - { - BusinessException => LogLevel.Warning, - ValidationException => LogLevel.Warning, - NotFoundException => LogLevel.Information, - _ => LogLevel.Error - }; - - _logger.Log(logLevel, exception, - "Error ID: {ErrorId} - Path: {Path} - Method: {Method} - {@error}", + var errorId = Guid.NewGuid().ToString(); + _logger.LogError( + exception, + "Unhandled exception - RequestId: {RequestId}, ErrorId: {ErrorId}, Message: {Message}", + requestId, errorId, - context.Request.Path, - context.Request.Method, - error); + exception.Message); + + LogError(errorId, context, exception); + await HandleExceptionAsync(context, errorId, exception); } + } - // Update HandleExceptionAsync to use the new ErrorResponse format - private async Task HandleExceptionAsync(HttpContext context, string errorId, Exception exception) + private void LogError(string errorId, HttpContext context, Exception exception) + { + var error = new { - var statusCode = exception switch - { - ApiException apiEx => (int)apiEx.StatusCode, - InvalidOperationException => StatusCodes.Status400BadRequest, - _ => StatusCodes.Status500InternalServerError - }; - - // Add specific handling for TooManyRequestsException - if (exception is TooManyRequestsException tooManyRequestsEx && tooManyRequestsEx.RetryAfter.HasValue) - { - // Add Retry-After header in seconds - var retryAfterSeconds = Math.Max(1, (int)(tooManyRequestsEx.RetryAfter.Value - DateTime.UtcNow).TotalSeconds); - context.Response.Headers.Append("Retry-After", retryAfterSeconds.ToString()); - context.Response.Headers.Append("X-RateLimit-Reset", tooManyRequestsEx.RetryAfter.Value.ToString("r")); - } - - // Generate a general message based on status code - var generalMessage = statusCode switch - { - 400 => "Bad Request - The request was invalid or cannot be served", - 401 => "Unauthorized - Authentication credentials are missing or invalid", - 403 => "Forbidden - You don't have permission to access this resource", - 404 => "Not Found - The requested resource does not exist", - 429 => "Too Many Requests - Rate limit exceeded", - 500 => "Internal Server Error - Something went wrong on our end", - _ => "An error occurred" - }; - - // Get specific reason from the exception - var reason = _env.IsDevelopment() || exception is ApiException - ? exception.Message - : "An unexpected error occurred"; - - // Collect specific error details - var errors = new List(); - - // Add rate limiting specific errors - if (exception is TooManyRequestsException tooManyReq && tooManyReq.RetryAfter.HasValue) - { - errors.Add($"Too many requests. Please retry after {tooManyReq.RetryAfter.Value:G}"); - } - - // Add validation errors if present - if (exception is ValidationException validationEx && validationEx.Errors != null && validationEx.Errors.Any()) - { - errors.AddRange(validationEx.Errors); - } - - // Create the enhanced ErrorResponse format - var errorResponse = new ErrorResponse - { - Message = generalMessage, - Reason = reason, - Errors = errors.Count > 0 ? errors : null // Only include errors if there are any - }; - - context.Response.ContentType = "application/json"; - context.Response.StatusCode = statusCode; - - if (!context.Response.HasStarted) - { - await context.Response.WriteAsJsonAsync(errorResponse); - } - } + ErrorId = errorId, + Timestamp = DateTime.UtcNow, + RequestPath = context.Request.Path, + RequestMethod = context.Request.Method, + ExceptionType = exception.GetType().Name, + ExceptionMessage = exception.Message + }; + + var logLevel = exception switch + { + BusinessException => LogLevel.Warning, + ValidationException => LogLevel.Warning, + NotFoundException => LogLevel.Information, + _ => LogLevel.Error + }; + + _logger.Log(logLevel, exception, + "Error ID: {ErrorId} - Path: {Path} - Method: {Method} - {@error}", + errorId, + context.Request.Path, + context.Request.Method, + error); } - // Extension method remains the same - public static class ExceptionHandlerMiddlewareExtensions + // Update HandleExceptionAsync to use the new ErrorResponse format + private async Task HandleExceptionAsync(HttpContext context, string errorId, Exception exception) { - public static IApplicationBuilder UseApiExceptionHandler(this IApplicationBuilder builder) + var statusCode = exception switch + { + ApiException apiEx => (int)apiEx.StatusCode, + InvalidOperationException => StatusCodes.Status400BadRequest, + _ => StatusCodes.Status500InternalServerError + }; + + // Add specific handling for TooManyRequestsException + if (exception is TooManyRequestsException tooManyRequestsEx && tooManyRequestsEx.RetryAfter.HasValue) { - return builder.UseMiddleware(); + // Add Retry-After header in seconds + var retryAfterSeconds = + Math.Max(1, (int)(tooManyRequestsEx.RetryAfter.Value - DateTime.UtcNow).TotalSeconds); + context.Response.Headers.Append("Retry-After", retryAfterSeconds.ToString()); + context.Response.Headers.Append("X-RateLimit-Reset", tooManyRequestsEx.RetryAfter.Value.ToString("r")); } + + // Generate a general message based on status code + var generalMessage = statusCode switch + { + 400 => "Bad Request - The request was invalid or cannot be served", + 401 => "Unauthorized - Authentication credentials are missing or invalid", + 403 => "Forbidden - You don't have permission to access this resource", + 404 => "Not Found - The requested resource does not exist", + 409 => "Conflict - The request conflicts with current state", + 429 => "Too Many Requests - Rate limit exceeded", + 500 => "Internal Server Error - Something went wrong on our end", + _ => "An error occurred" + }; + + // Get specific error message from the exception + var errorMessage = _env.IsDevelopment() || exception is ApiException + ? exception.Message + : "An unexpected error occurred"; + + // Collect specific error details + var errors = new List(); + + // Add rate limiting specific errors + if (exception is TooManyRequestsException tooManyReq && tooManyReq.RetryAfter.HasValue) + errors.Add($"Too many requests. Please retry after {tooManyReq.RetryAfter.Value:G}"); + + // Add validation errors if present + if (exception is ValidationException validationEx && validationEx.Errors != null && validationEx.Errors.Any()) + errors.AddRange(validationEx.Errors); + + // Create the enhanced ErrorResponse format + var errorResponse = new ErrorResponse + { + Message = generalMessage, + Reason = errorMessage, + Errors = errors.Count > 0 ? errors : null // Only include errors if there are any + }; + + context.Response.ContentType = "application/json"; + context.Response.StatusCode = statusCode; + + if (!context.Response.HasStarted) await context.Response.WriteAsJsonAsync(errorResponse); } } + +// Extension method remains the same +public static class ExceptionHandlerMiddlewareExtensions +{ + public static IApplicationBuilder UseApiExceptionHandler(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/AIModel/ModelPromting.cs b/TaskGenix.Core/Requests/AIModel/ModelPromting.cs new file mode 100644 index 0000000..31fc1d1 --- /dev/null +++ b/TaskGenix.Core/Requests/AIModel/ModelPromting.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Threading.Tasks; + +namespace TaskGenix.Core.Requests.AIModel +{ + public class ModelPromting + { + public static string GetSummarizePromptTemplate() + { + return + """ + + You are an AI meeting assistant specialized in summarizing meeting transcripts + into clear and structured summaries. + + # Objective + - Read the entire meeting transcript carefully. + - Extract key discussion topics, main ideas, and decisions made. + - Summarize them in a structured format that's easy to review later. + + # Localization + - The user's preferred language is: {{locale}} + - Respond entirely in this language (including section titles and text). + - However, always preserve academic, technical, or on-field major terms in their original form (do not translate these terms). + - Do NOT mix languages unless necessary for names, technical terms, or academic/major-specific terminology. + + # Output Format (translated to user's locale, except preserved terms) + ### Meeting Summary + **Main Topics:** + - ... + + **Key Discussion Points:** + - ... + + **Decisions / Agreements:** + - ... + + **Action Items / Next Steps:** + - ... + + # Style + - Be concise, professional, and neutral. + - Do NOT include unnecessary details or filler text. + - Ignore timestamps (e.g., [00:01:24]) or filler phrases such as: + - English: "uh", "yeah", "like", "you know" + - Vietnamese: "ờ", "ừm", "kiểu như", "thì", "à", "như là" + - Japanese: "えっと", "あの", "その", "うん", "まぁ" + - Focus only on meaningful content or decisions. + + + + Please summarize the following meeting transcript: + + {{transcript}} + + """; + } + + + public static string GetGenerateTaskWithResourcesPromptTemplate() + { + return + """ + + You are an AI project assistant that analyzes summarized meeting notes and + generates actionable tasks for the project team. + + Additionally, for each task, you will recommend useful learning or reference resources + (articles, documentation, tutorials, videos, or blogs) from the web + to help team members complete the task effectively. + + # Localization + - The user's preferred language is: {{locale}}. + - Generate task titles, descriptions, and resource descriptions in this language. + - Always preserve technical, academic, or field-specific terms in their original form (do not translate these terms). + - Do NOT mix languages unless necessary for names or technical terms. + - If possible, recommend resources (links, videos, etc.) that are relevant and available in this language. + + # Objective + - Read the meeting summary carefully. + - Identify all *specific, actionable tasks* mentioned. + - For each task: + - Create a clear **title** and concise **description**. + - Infer or extract **dueDate** (YYYY-MM-DD). + - Assign **priority** (Low / Medium / High / Critical). + - Default **status** = "Pending". + - Suggest **1 to 3 resource links** with: + - title + - url + - description + - type + + # Resource Type Strict Rule + The field `"type"` MUST be **exactly one** of the following string values: + - "Article" + - "Documentation" + - "Video" + - "Tutorial" + - "Blog" + - "News" + - "Other" + + Absolutely DO NOT create or output any other type values. + If a resource does not fit any of the above categories, use `"Other"`. + + # Output Format + Return ONLY a valid JSON array (no markdown, no commentary, no text before or after): + + [ + { + "title": "string", + "description": "string", + "dueDate": "YYYY-MM-DD", + "priority": "Low | Medium | High | Critical", + "status": "Pending", + "resourceLinks": [ + { + "title": "string", + "url": "https://...", + "description": "string", + "type": "Article | Documentation | Video | Tutorial | Blog | News | Other" + } + ] + } + ] + + # Validation Checklist (before final output) + - Verify that the JSON is valid. + - Verify that all "type" fields are one of the 7 allowed values. + - If unsure about type, default to "Other". + - Do not include explanations, comments, or any text outside the JSON array. + + + + Based on the following meeting summary, extract all actionable tasks and recommend resources for each: + + {{summary}} + + """; + } + + } + + public class CreateSummaryRequest + { + [Required] + public string Transcript { get; set; } + public string Locale { get; set; } = "en-US"; + } + + public class GenerateTasksRequest + { + [Required] + public string SummaryText { get; set; } + public string Locale { get; set; } = "en-US"; + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Attachment/AttachmentRequest.cs b/TaskGenix.Core/Requests/Attachment/AttachmentRequest.cs new file mode 100644 index 0000000..f1926fa --- /dev/null +++ b/TaskGenix.Core/Requests/Attachment/AttachmentRequest.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace TaskGenix.Core.Requests.Attachment +{ + public class AttachmentRequest + { + + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Auth/Auth0OnboardingRequest.cs b/TaskGenix.Core/Requests/Auth/Auth0OnboardingRequest.cs new file mode 100644 index 0000000..3c68e5b --- /dev/null +++ b/TaskGenix.Core/Requests/Auth/Auth0OnboardingRequest.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.Auth; + +public class Auth0OnboardingRequest +{ + [Required] + [MaxLength(128)] + public string Auth0UserId { get; set; } = null!; + + [Required] + [EmailAddress] + [MaxLength(100)] + public string Email { get; set; } = null!; + + [Required] + [MaxLength(100)] + public string Username { get; set; } = null!; + + [Required] + public string InternalSecret { get; set; } = null!; +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Auth/AuthRequest.cs b/TaskGenix.Core/Requests/Auth/AuthRequest.cs new file mode 100644 index 0000000..2cfbf92 --- /dev/null +++ b/TaskGenix.Core/Requests/Auth/AuthRequest.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using Swashbuckle.AspNetCore.Annotations; + +namespace TaskGenix.Core.Requests.Auth; + +public class LoginRequest +{ + [Required] [EmailAddress] public string Email { get; set; } = null!; + + [Required] public string Password { get; set; } = null!; +} + +public class RegisterRequest +{ + [Required] [MaxLength(100)] public string Username { get; set; } = null!; + + [Required] [EmailAddress] public string Email { get; set; } = null!; + + [Required] [MinLength(6)] public string Password { get; set; } = null!; +} + +public class RefreshTokenRequest +{ + [Required] public string RefreshToken { get; set; } = null!; +} + +public class MobileLoginRequest +{ + [Required] [EmailAddress] public string Email { get; set; } = null!; +} + +public class VerifyOtpRequest +{ + [Required] [EmailAddress] public string Email { get; set; } = null!; + + [Required] [Length(6, 6)] public string Code { get; set; } = null!; +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Notification/NotificationRequests.cs b/TaskGenix.Core/Requests/Notification/NotificationRequests.cs new file mode 100644 index 0000000..686550c --- /dev/null +++ b/TaskGenix.Core/Requests/Notification/NotificationRequests.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Requests.Notification; + +public class CreateNotificationRequest +{ + [Required] public string Title { get; set; } = null!; + + [Required] public string Message { get; set; } = null!; + + public NotificationType Type { get; set; } = NotificationType.INFO; + public NotificationIcon Icon { get; set; } = NotificationIcon.MESSAGE; + public NotificationColor Color { get; set; } = NotificationColor.BLUE; + + public Guid? TaskGenId { get; set; } + + public DateTime? ScheduledTime { get; set; } +} + +public class SendNotificationRequest +{ + [Required] public string Token { get; set; } = null!; + + [Required] public string Title { get; set; } = null!; + + [Required] public string Body { get; set; } = null!; +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Payment/PayOSConfig.cs b/TaskGenix.Core/Requests/Payment/PayOSConfig.cs new file mode 100644 index 0000000..1dd4e11 --- /dev/null +++ b/TaskGenix.Core/Requests/Payment/PayOSConfig.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaskGenix.Core.Requests.Payment +{ + public class PayOSConfig + { + public string ClientId { get; set; } = null!; + public string ApiKey { get; set; } = null!; + public string ChecksumKey { get; set; } = null!; + } +} diff --git a/TaskGenix.Core/Requests/Payment/PaymentRequest.cs b/TaskGenix.Core/Requests/Payment/PaymentRequest.cs new file mode 100644 index 0000000..3c1fb0e --- /dev/null +++ b/TaskGenix.Core/Requests/Payment/PaymentRequest.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.Payment +{ + public class PaymentRequest + { + [Required] + public decimal Amount { get; set; } + + [Required] + [MaxLength(3)] + public string Currency { get; set; } = "USD"; + + [Required] + [MaxLength(50)] + public string PaymentMethod { get; set; } = null!; + + [Required] + public Guid UserId { get; set; } + + [Required] + public Guid UserPlanId { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Payment/VerifyIapRequest.cs b/TaskGenix.Core/Requests/Payment/VerifyIapRequest.cs new file mode 100644 index 0000000..f49d239 --- /dev/null +++ b/TaskGenix.Core/Requests/Payment/VerifyIapRequest.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.Payment +{ + public class VerifyIapRequest + { + [Required] + [MaxLength(100)] + public string ProductId { get; set; } = null!; + + [Required] + [MaxLength(500)] + public string PurchaseId { get; set; } = null!; + + [Required] + public Guid UserId { get; set; } + + [Required] + public Guid UserPlanId { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Project/CreateProjectRequest.cs b/TaskGenix.Core/Requests/Project/CreateProjectRequest.cs new file mode 100644 index 0000000..d624df4 --- /dev/null +++ b/TaskGenix.Core/Requests/Project/CreateProjectRequest.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Requests.Project; + +public class CreateProjectRequest +{ + public string? Name { get; set; } + public string? Description { get; set; } + public ProjectStatus Status { get; set; } = ProjectStatus.NotStarted; + public ProjectType Type { get; set; } + public Guid OwnerId { get; set; } + public List? MemberIds { get; set; } + public List? TaskGenIds { get; set; } + public DateTime? DueDate { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Project/UpdateProjectRequest.cs b/TaskGenix.Core/Requests/Project/UpdateProjectRequest.cs new file mode 100644 index 0000000..da1c22e --- /dev/null +++ b/TaskGenix.Core/Requests/Project/UpdateProjectRequest.cs @@ -0,0 +1,18 @@ +using System; +using System.ComponentModel.DataAnnotations; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Requests.Project; + +public class UpdateProjectRequest +{ + [MaxLength(200)] public string? Name { get; set; } + + [MaxLength(2000)] public string? Description { get; set; } + + public ProjectStatus? Status { get; set; } + + public ProjectType? Type { get; set; } + + public DateTime? DueDate { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/ProjectMember/AddProjectMemberRequest.cs b/TaskGenix.Core/Requests/ProjectMember/AddProjectMemberRequest.cs new file mode 100644 index 0000000..346f179 --- /dev/null +++ b/TaskGenix.Core/Requests/ProjectMember/AddProjectMemberRequest.cs @@ -0,0 +1,12 @@ +using System; +using System.ComponentModel.DataAnnotations; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Requests.ProjectMember; + +public class AddProjectMemberRequest +{ + [Required] public Guid UserId { get; set; } + + [Required] public MemberRole Role { get; set; } = MemberRole.Member; +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/ProjectMember/UpdateProjectMemberRequest.cs b/TaskGenix.Core/Requests/ProjectMember/UpdateProjectMemberRequest.cs new file mode 100644 index 0000000..a6f2292 --- /dev/null +++ b/TaskGenix.Core/Requests/ProjectMember/UpdateProjectMemberRequest.cs @@ -0,0 +1,10 @@ +using System; +using System.ComponentModel.DataAnnotations; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Requests.ProjectMember; + +public class UpdateProjectMemberRequest +{ + [Required] public MemberRole Role { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Recording/CreateRecordingRequest.cs b/TaskGenix.Core/Requests/Recording/CreateRecordingRequest.cs new file mode 100644 index 0000000..0aa8d97 --- /dev/null +++ b/TaskGenix.Core/Requests/Recording/CreateRecordingRequest.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.Recording +{ + public class CreateRecordingRequest + { + [Required] + [MaxLength(200)] + public string Title { get; set; } = null!; + + [Required] + public TimeSpan? Duration { get; set; } + + public long FileSize { get; set; } + + [MaxLength(100)] + public string? MimeType { get; set; } + + public Guid? ProjectId { get; set; } + + [Required] + public string OwnerEmail { get; set; } = null!; + + public Guid? OwnerId { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Recording/UpdateRecordingRequest.cs b/TaskGenix.Core/Requests/Recording/UpdateRecordingRequest.cs new file mode 100644 index 0000000..afce86d --- /dev/null +++ b/TaskGenix.Core/Requests/Recording/UpdateRecordingRequest.cs @@ -0,0 +1,35 @@ +using System; +using System.ComponentModel.DataAnnotations; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Requests.Recording +{ + public class UpdateRecordingRequest + { + [MaxLength(200)] + public string? Title { get; set; } + + public string? ObjectKey { get; set; } + + public TimeSpan? Duration { get; set; } + + public long? FileSize { get; set; } + + [MaxLength(100)] + public string? MimeType { get; set; } + + public Guid? ProjectId { get; set; } + + public string? OwnerEmail { get; set; } + + public Guid? OwnerId { get; set; } + + public bool? IsTranscriptRequested { get; set; } + + public TranscriptStatus? TranscriptStatus { get; set; } + + public string? TranscriptText { get; set; } + + public string? TranscriptErrorMessage { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/Recording/UploadRecordingRequest.cs b/TaskGenix.Core/Requests/Recording/UploadRecordingRequest.cs new file mode 100644 index 0000000..999b600 --- /dev/null +++ b/TaskGenix.Core/Requests/Recording/UploadRecordingRequest.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.Recording +{ + public class UploadRecordingRequest + { + [Required] + public IFormFile File { get; set; } = null!; + + [Required] + public string Title { get; set; } = null!; + + [Required] + public string OwnerEmail { get; set; } = null!; + + public string? TranscriptText { get; set; } + public TimeSpan? Duration { get; set; } + public string? MimeType { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/ResourceLink/ResourceLinkRequest.cs b/TaskGenix.Core/Requests/ResourceLink/ResourceLinkRequest.cs new file mode 100644 index 0000000..8df1822 --- /dev/null +++ b/TaskGenix.Core/Requests/ResourceLink/ResourceLinkRequest.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.ComponentModel.DataAnnotations; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Requests.ResourceLink +{ + public class ResourceLinkRequest + { + [Required] + [MaxLength(200)] + public string Title { get; set; } = null!; + + [Required] + public string Url { get; set; } = null!; + + public string? Description { get; set; } + + [Required] + public ResourceType Type { get; set; } = ResourceType.Other; + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/TaskAssignment/AssignUserToTaskRequest.cs b/TaskGenix.Core/Requests/TaskAssignment/AssignUserToTaskRequest.cs new file mode 100644 index 0000000..1ddf270 --- /dev/null +++ b/TaskGenix.Core/Requests/TaskAssignment/AssignUserToTaskRequest.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.TaskAssignment; + +public class AssignUserToTaskRequest +{ + [Required(ErrorMessage = "Task ID is required")] + public Guid TaskId { get; set; } + + [Required(ErrorMessage = "User ID is required")] + public Guid UserId { get; set; } +} diff --git a/TaskGenix.Core/Requests/TaskAssignment/BulkAssignUsersRequest.cs b/TaskGenix.Core/Requests/TaskAssignment/BulkAssignUsersRequest.cs new file mode 100644 index 0000000..285e4d7 --- /dev/null +++ b/TaskGenix.Core/Requests/TaskAssignment/BulkAssignUsersRequest.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.TaskAssignment; + +public class BulkAssignUsersRequest +{ + [Required(ErrorMessage = "Task ID is required")] + public Guid TaskId { get; set; } + + [Required(ErrorMessage = "At least one user ID is required")] + [MinLength(1, ErrorMessage = "At least one user ID is required")] + public List UserIds { get; set; } = new(); +} diff --git a/TaskGenix.Core/Requests/TaskAssignment/RemoveUserFromTaskRequest.cs b/TaskGenix.Core/Requests/TaskAssignment/RemoveUserFromTaskRequest.cs new file mode 100644 index 0000000..16c7d18 --- /dev/null +++ b/TaskGenix.Core/Requests/TaskAssignment/RemoveUserFromTaskRequest.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.TaskAssignment; + +public class RemoveUserFromTaskRequest +{ + [Required(ErrorMessage = "Task ID is required")] + public Guid TaskId { get; set; } + + [Required(ErrorMessage = "User ID is required")] + public Guid UserId { get; set; } +} diff --git a/TaskGenix.Core/Requests/TaskGen/CreateTaskRequest.cs b/TaskGenix.Core/Requests/TaskGen/CreateTaskRequest.cs index def93b8..f44b8a3 100644 --- a/TaskGenix.Core/Requests/TaskGen/CreateTaskRequest.cs +++ b/TaskGenix.Core/Requests/TaskGen/CreateTaskRequest.cs @@ -2,29 +2,24 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.ResourceLink; -namespace TaskGenix.Core.Requests.TaskGen +namespace TaskGenix.Core.Requests.TaskGen; + +public class CreateTaskRequest { - public class CreateTaskRequest - { - [Required] - [MaxLength(200)] - public string Title { get; set; } = null!; + [Required][MaxLength(200)] public string Title { get; set; } = null!; - [MaxLength(1000)] - public string? Description { get; set; } + [MaxLength(1000)] public string? Description { get; set; } - [Required] - public DateTime? DueDate { get; set; } + [Required] public DateTime? DueDate { get; set; } - [Required] - public Guid ProjectId { get; set; } + [Required] public Guid ProjectId { get; set; } - [Required] - public TaskGenStatus Status { get; set; } = TaskGenStatus.Pending; + [Required] public TaskGenStatus Status { get; set; } = TaskGenStatus.Pending; - public int Priority { get; set; } = 0; + public TaskGenPriority Priority { get; set; } = TaskGenPriority.Low; - public List? AssignedUserIds { get; set; } - } -} + public List? AssignedUserIds { get; set; } + public List? ResourceLinks { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/TaskGen/UpdateTaskRequest.cs b/TaskGenix.Core/Requests/TaskGen/UpdateTaskRequest.cs index fcac45c..c38d02b 100644 --- a/TaskGenix.Core/Requests/TaskGen/UpdateTaskRequest.cs +++ b/TaskGenix.Core/Requests/TaskGen/UpdateTaskRequest.cs @@ -3,22 +3,19 @@ using System.ComponentModel.DataAnnotations; using TaskGenix.Core.Domains; -namespace TaskGenix.Core.Requests.TaskGen +namespace TaskGenix.Core.Requests.TaskGen; + +public class UpdateTaskRequest { - public class UpdateTaskRequest - { - [MaxLength(200)] - public string? Title { get; set; } + [MaxLength(200)] public string? Title { get; set; } - [MaxLength(1000)] - public string? Description { get; set; } + [MaxLength(1000)] public string? Description { get; set; } - public DateTime? DueDate { get; set; } + public DateTime? DueDate { get; set; } - public TaskGenStatus Status { get; set; } + public TaskGenStatus Status { get; set; } - public TaskGenPriority Priority { get; set; } + public TaskGenPriority Priority { get; set; } - public List? AssignedUserIds { get; set; } - } -} + public List? AssignedUserIds { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/UserDeviceToken/UserDeviceTokenRequests.cs b/TaskGenix.Core/Requests/UserDeviceToken/UserDeviceTokenRequests.cs new file mode 100644 index 0000000..7fde183 --- /dev/null +++ b/TaskGenix.Core/Requests/UserDeviceToken/UserDeviceTokenRequests.cs @@ -0,0 +1,66 @@ +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.UserDeviceToken +{ + public class CreateUserDeviceTokenRequest + { + public Guid? UserId { get; set; } // Will be set from JWT token if not provided + + [Required] + public string DeviceId { get; set; } = null!; + + [Required] + public string FCMToken { get; set; } = null!; + + public string? DeviceName { get; set; } + + [Required] + public string Platform { get; set; } = null!; + } + + public class CreateUserDeviceTokenRequestV2 + { + [Required] + [EmailAddress] + public string Email { get; set; } = null!; // Will be set from JWT token if not provided + + [Required] + public string DeviceId { get; set; } = null!; + + [Required] + public string FCMToken { get; set; } = null!; + + [Required] + public string DeviceName { get; set; } = null!; + + [Required] + public string Platform { get; set; } = null!; + } + + public class UpdateUserDeviceTokenRequest + { + public string? FCMToken { get; set; } + public string? DeviceName { get; set; } + public string? Platform { get; set; } + } + + public class RegisterTokenWithEmailRequest + { + [Required] + [EmailAddress] + public string Email { get; set; } = null!; + + [Required] + public string DeviceId { get; set; } = null!; + + [Required] + [RegularExpression(@"^[a-zA-Z0-9\-_:]+$", ErrorMessage = "Invalid FCM token format")] + public string FCMToken { get; set; } = null!; + + [Required] + public string DeviceName { get; set; } = null!; + + [Required] + public string Platform { get; set; } = null!; + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/UserPlan/UpdateUserPlanRequest.cs b/TaskGenix.Core/Requests/UserPlan/UpdateUserPlanRequest.cs new file mode 100644 index 0000000..fcb90d6 --- /dev/null +++ b/TaskGenix.Core/Requests/UserPlan/UpdateUserPlanRequest.cs @@ -0,0 +1,27 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.UserPlan +{ + public class UpdateUserPlanRequest + { + [Required] + public string Name { get; set; } = null!; + + [Required] + public decimal Price { get; set; } + + [Required] + public Guid UserId { get; set; } + + [Required] + public Guid PlanId { get; set; } + + public DateTime StartDate { get; set; } + + [Required] + public DateTime EndDate { get; set; } + + public bool IsActive { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/UserPlan/UserPlanRequest.cs b/TaskGenix.Core/Requests/UserPlan/UserPlanRequest.cs new file mode 100644 index 0000000..5ff1739 --- /dev/null +++ b/TaskGenix.Core/Requests/UserPlan/UserPlanRequest.cs @@ -0,0 +1,28 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace TaskGenix.Core.Requests.UserPlan +{ + public class UserPlanRequest + { + [Required] + public string Name { get; set; } = null!; + + [Required] + public decimal Price { get; set; } + + [Required] + public Guid UserId { get; set; } + + [Required] + public Guid PlanId { get; set; } + + [Required] + public DateTime StartDate { get; set; } + + [Required] + public DateTime EndDate { get; set; } + + public bool IsActive { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Requests/UserPlanFeature/CreateUserPlanFeatureRequest.cs b/TaskGenix.Core/Requests/UserPlanFeature/CreateUserPlanFeatureRequest.cs new file mode 100644 index 0000000..91226cb --- /dev/null +++ b/TaskGenix.Core/Requests/UserPlanFeature/CreateUserPlanFeatureRequest.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaskGenix.Core.Requests.UserPlanFeature +{ + public class CreateUserPlanFeatureRequest + { + [Required] + public Guid UserPlanId { get; set; } + + [Required] + public Guid PlanFeatureId { get; set; } + + public decimal QuotaTotal { get; set; } + + public decimal QuotaUsed { get; set; } = 0m; + + public bool IsUnlimited { get; set; } = false; + + // Cached key copied from Feature.Key via PlanFeature -> Feature + // Required to match the domain UserPlanFeature.FeatureKey + [Required] + [MaxLength(50)] + public string FeatureKey { get; set; } = null!; + } +} diff --git a/TaskGenix.Core/Requests/UserPlanFeature/UpdateUserPlanFeatureRequest.cs b/TaskGenix.Core/Requests/UserPlanFeature/UpdateUserPlanFeatureRequest.cs new file mode 100644 index 0000000..6bf7dbd --- /dev/null +++ b/TaskGenix.Core/Requests/UserPlanFeature/UpdateUserPlanFeatureRequest.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaskGenix.Core.Requests.UserPlanFeature +{ + public class UpdateUserPlanFeatureRequest + { + [Required] + public Guid UserPlanId { get; set; } + + public decimal? QuotaUsed { get; set; } + + [Required] + [MaxLength(50)] + public string FeatureKey { get; set; } = null!; + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Auth/Auth0OnboardingResponse.cs b/TaskGenix.Core/Responses/Auth/Auth0OnboardingResponse.cs new file mode 100644 index 0000000..7814688 --- /dev/null +++ b/TaskGenix.Core/Responses/Auth/Auth0OnboardingResponse.cs @@ -0,0 +1,9 @@ +namespace TaskGenix.Core.Responses.Auth; + +public class Auth0OnboardingResponse +{ + public Guid InternalUserId { get; set; } + public string Email { get; set; } = null!; + public string Username { get; set; } = null!; + public bool IsNewUser { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Auth/AuthResponse.cs b/TaskGenix.Core/Responses/Auth/AuthResponse.cs new file mode 100644 index 0000000..59d5def --- /dev/null +++ b/TaskGenix.Core/Responses/Auth/AuthResponse.cs @@ -0,0 +1,31 @@ +namespace TaskGenix.Core.Responses.Auth; + +public class LoginResponse +{ + public TokenResponse Token { get; set; } = null!; + public UserInfo Account { get; set; } = null!; +} + +public class TokenResponse +{ + public string AccessToken { get; set; } = null!; + public string RefreshToken { get; set; } = null!; + public string RefreshExpirationDate { get; set; } = null!; + public string ExpirationDate { get; set; } = null!; +} + +public class UserInfo +{ + public Guid Id { get; set; } + public string Username { get; set; } = null!; + public string Email { get; set; } = null!; + public string Role { get; set; } = null!; +} + +public class ResendOtpResponse +{ + public string Message { get; set; } = null!; + public string Email { get; set; } = null!; + public int RemainingAttempts { get; set; } + public int ExpiresInMinutes { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Feature/FeatureResponse.cs b/TaskGenix.Core/Responses/Feature/FeatureResponse.cs index d320e65..e888f48 100644 --- a/TaskGenix.Core/Responses/Feature/FeatureResponse.cs +++ b/TaskGenix.Core/Responses/Feature/FeatureResponse.cs @@ -1,14 +1,13 @@ using System; using TaskGenix.Core.Domains; -namespace TaskGenix.Core.Responses.Feature +namespace TaskGenix.Core.Responses.Feature; + +public class FeatureResponse { - public class FeatureResponse - { - public Guid Id { get; set; } - public string Key { get; set; } = null!; - public string Name { get; set; } = null!; - public string Unit { get; set; } = null!; - public FeatureType FeatureType { get; set; } - } + public Guid Id { get; set; } + public string Key { get; set; } = null!; + public string Name { get; set; } = null!; + public string Unit { get; set; } = null!; + public FeatureType FeatureType { get; set; } } \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Notification/NotificationResponse.cs b/TaskGenix.Core/Responses/Notification/NotificationResponse.cs new file mode 100644 index 0000000..8db2c93 --- /dev/null +++ b/TaskGenix.Core/Responses/Notification/NotificationResponse.cs @@ -0,0 +1,23 @@ +namespace TaskGenix.Core.Responses.Notification; + +public class NotificationResponse +{ + public Guid Id { get; set; } + public string Title { get; set; } = null!; + public string Message { get; set; } = null!; + public string Type { get; set; } = null!; + public string Icon { get; set; } = null!; + public string Color { get; set; } = null!; + public bool IsSent { get; set; } + public bool IsRead { get; set; } + + public DateTime? ScheduledTime { get; set; } + + // public DateTime? SentAt { get; set; } + public DateTime CreatedAt { get; set; } + + public DateTime UpdatedAt { get; set; } + + // public Guid UserId { get; set; } + public Guid? TaskGenId { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/PaymentResponse/PaymentResponse.cs b/TaskGenix.Core/Responses/PaymentResponse/PaymentResponse.cs new file mode 100644 index 0000000..09a03b1 --- /dev/null +++ b/TaskGenix.Core/Responses/PaymentResponse/PaymentResponse.cs @@ -0,0 +1,10 @@ +namespace TaskGenix.Core.Responses.PaymentResponse +{ + public class PaymentResponse + { + public bool Success { get; set; } + public string? Message { get; set; } + public string? TransactionId { get; set; } + public string? CheckoutUrl { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Plan/PlanFeatureResponse.cs b/TaskGenix.Core/Responses/Plan/PlanFeatureResponse.cs index 04555f5..1771be1 100644 --- a/TaskGenix.Core/Responses/Plan/PlanFeatureResponse.cs +++ b/TaskGenix.Core/Responses/Plan/PlanFeatureResponse.cs @@ -2,12 +2,12 @@ using TaskGenix.Core.Domains; using TaskGenix.Core.Responses.Feature; -namespace TaskGenix.Core.Responses.Plan +namespace TaskGenix.Core.Responses.Plan; + +public class PlanFeatureResponse { - public class PlanFeatureResponse - { - public decimal? QuotaValue { get; set; } - public bool IsUnlimited { get; set; } - public FeatureResponse Feature { get; set; } = null!; - } + public Guid Id { get; set; } + public decimal? QuotaValue { get; set; } + public bool IsUnlimited { get; set; } + public FeatureResponse Feature { get; set; } = null!; } \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Plan/PlanResponse.cs b/TaskGenix.Core/Responses/Plan/PlanResponse.cs index 7cbc7ce..513f88b 100644 --- a/TaskGenix.Core/Responses/Plan/PlanResponse.cs +++ b/TaskGenix.Core/Responses/Plan/PlanResponse.cs @@ -3,16 +3,15 @@ using System.Linq; using System.Threading.Tasks; -namespace TaskGenix.Core.Responses.Plan +namespace TaskGenix.Core.Responses.Plan; + +public class PlanResponse { - public class PlanResponse - { - public Guid Id { get; set; } - public string Name { get; set; } = null!; - public string Description { get; set; } = null!; + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public string Description { get; set; } = null!; - public int DurationInDays { get; set; } - public decimal Price { get; set; } - public List Features { get; set; } = new List(); - } + public int DurationInDays { get; set; } + public decimal Price { get; set; } + public List Features { get; set; } = new(); } \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Project/ProjectMemberResponse.cs b/TaskGenix.Core/Responses/Project/ProjectMemberResponse.cs index 1cdcf42..1da75df 100644 --- a/TaskGenix.Core/Responses/Project/ProjectMemberResponse.cs +++ b/TaskGenix.Core/Responses/Project/ProjectMemberResponse.cs @@ -7,14 +7,15 @@ using TaskGenix.Core.Domains; using TaskGenix.Core.Responses.User; -namespace TaskGenix.Core.Responses.Project -{ - public class ProjectMemberResponse - { - public UserResponse User { get; set; } = null!; - - public MemberRole Role { get; set; } +namespace TaskGenix.Core.Responses.Project; - public DateTime JoinedAt { get; set; } = DateTime.UtcNow; - } -} +public class ProjectMemberResponse +{ + public Guid Id { get; set; } + public Guid ProjectId { get; set; } + public UserResponse User { get; set; } = null!; + public MemberRole Role { get; set; } + public DateTime JoinedAt { get; set; } = DateTime.UtcNow; + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Project/ProjectProgressResponse.cs b/TaskGenix.Core/Responses/Project/ProjectProgressResponse.cs new file mode 100644 index 0000000..0e005e1 --- /dev/null +++ b/TaskGenix.Core/Responses/Project/ProjectProgressResponse.cs @@ -0,0 +1,14 @@ +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Responses.Project; + +public class ProjectProgressResponse +{ + public Guid ProjectId { get; set; } + public decimal Progress { get; set; } + public int TotalTasks { get; set; } + public int CompletedTasks { get; set; } + public int InProgressTasks { get; set; } + public Dictionary TasksByPriority { get; set; } = new(); + public Dictionary TasksByStatus { get; set; } = new(); +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Project/ProjectResponse.cs b/TaskGenix.Core/Responses/Project/ProjectResponse.cs index 29275b3..a253a6c 100644 --- a/TaskGenix.Core/Responses/Project/ProjectResponse.cs +++ b/TaskGenix.Core/Responses/Project/ProjectResponse.cs @@ -1,18 +1,22 @@ using System; using TaskGenix.Core.Responses.TaskGen; +using TaskGenix.Core.Responses.User; -namespace TaskGenix.Core.Responses.Project +namespace TaskGenix.Core.Responses.Project; + +public class ProjectResponse { - public class ProjectResponse - { - public Guid Id { get; set; } - public string? Name { get; set; } - public string? Description { get; set; } - public DateTime CreatedAt { get; set; } - public Guid OwnerId { get; set; } - public DateTime? UpdatedAt { get; set; } - public DateTime? DueDate { get; set; } - public List Members { get; set; } = new(); - public List TaskGens { get; set; } = new(); - } -} + public Guid Id { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public string? Status { get; set; } + public string? Type { get; set; } + public decimal Progress { get; set; } + public Guid OwnerId { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + public DateTime? DueDate { get; set; } + public bool IsActive { get; set; } + public List Members { get; set; } + public List TaskGens { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Project/WorkspaceSummaryResponse.cs b/TaskGenix.Core/Responses/Project/WorkspaceSummaryResponse.cs new file mode 100644 index 0000000..33e517e --- /dev/null +++ b/TaskGenix.Core/Responses/Project/WorkspaceSummaryResponse.cs @@ -0,0 +1,37 @@ +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Responses.Project; + +/// +/// Lightweight workspace summary for user dashboard +/// +public class WorkspaceSummaryResponse +{ + public Guid WorkspaceId { get; set; } + public string WorkspaceName { get; set; } = null!; + public string? Description { get; set; } + public ProjectStatus Status { get; set; } + public ProjectType Type { get; set; } + public decimal Progress { get; set; } + + // Task statistics + public int TotalTasks { get; set; } + public int CompletedTasks { get; set; } + public int InProgressTasks { get; set; } + public int OverdueTasks { get; set; } + public int PendingTasks { get; set; } + + // Timeline info + public DateTime? DueDate { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime? UpdatedAt { get; set; } + + // User's role in this workspace + public string UserRole { get; set; } = null!; + public bool IsOwner { get; set; } + + // Quick stats + public int MemberCount { get; set; } + public int HighPriorityTasks { get; set; } + public int CriticalTasks { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/Recording/RecordingResponse.cs b/TaskGenix.Core/Responses/Recording/RecordingResponse.cs new file mode 100644 index 0000000..d76fe22 --- /dev/null +++ b/TaskGenix.Core/Responses/Recording/RecordingResponse.cs @@ -0,0 +1,23 @@ +using System; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Responses.Recording +{ + public class RecordingResponse + { + public Guid Id { get; set; } + public string Title { get; set; } = null!; + public string ObjectKey { get; set; } = null!; + public TimeSpan? Duration { get; set; } + public long FileSize { get; set; } + public string? MimeType { get; set; } + public DateTimeOffset UploadedAt { get; set; } + public bool IsTranscriptRequested { get; set; } + public TranscriptStatus TranscriptStatus { get; set; } + public string? TranscriptText { get; set; } + public string? TranscriptErrorMessage { get; set; } + public Guid? ProjectId { get; set; } + public string? OwnerEmail { get; set; } + public Guid? OwnerId { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/SystemStatus/SystemStatusResponse.cs b/TaskGenix.Core/Responses/SystemStatus/SystemStatusResponse.cs new file mode 100644 index 0000000..6794d2d --- /dev/null +++ b/TaskGenix.Core/Responses/SystemStatus/SystemStatusResponse.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; + +namespace TaskGenix.Core.Responses.SystemStatus; + +/// +/// Represents the overall system health status +/// +public class SystemStatusResponse +{ + /// + /// Overall textual status of the system (e.g., "All Systems Operational") + /// + public string OverallStatus { get; set; } = "Operational"; + + /// + /// UTC time when the system status was checked + /// + public DateTime CheckedAt { get; set; } = DateTime.UtcNow; + + /// + /// List of individual service status entries + /// + public List Services { get; set; } = new(); + + /// + /// Metadata about the running environment + /// + public SystemMetadata Metadata { get; set; } = new(); +} + +/// +/// Describes the health of an individual service or subsystem +/// +public class ServiceStatusResponse +{ + public string ServiceName { get; set; } = null!; + + /// + /// The current health state of this service + /// + public ServiceHealthStatus Status { get; set; } + + /// + /// Additional descriptive message + /// + public string? Message { get; set; } + + /// + /// When this service was last checked + /// + public DateTime? LastChecked { get; set; } + + /// + /// Optional diagnostic data or metrics + /// + public Dictionary? AdditionalInfo { get; set; } + + /// + /// Optional latency measurement (in milliseconds) + /// + public double? ResponseTimeMs { get; set; } + + /// + /// Optional service version + /// + public string? Version { get; set; } +} + + +public enum ServiceHealthStatus +{ + Operational, + MinorOutage, + MajorOutage, + Maintenance, + Degraded +} + +/// +/// Metadata about the environment, app version, and uptime +/// +public class SystemMetadata +{ + public string Environment { get; set; } = "Production"; + public string ApiVersion { get; set; } = "v1.0.0"; + public string MachineName { get; set; } = "Server TaskGenix"; + public DateTime ServerStartTime { get; set; } = DateTime.UtcNow.AddHours(-1); + public double UptimeMinutes => (DateTime.UtcNow - ServerStartTime).TotalMinutes; + public string TraceId { get; set; } = Guid.NewGuid().ToString(); +} + + +public static class ServiceHealthStatusExtensions +{ + public static string ToDisplayString(this ServiceHealthStatus status) => status switch + { + ServiceHealthStatus.Operational => "Operational", + ServiceHealthStatus.MinorOutage => "Minor Outage", + ServiceHealthStatus.MajorOutage => "Major Outage", + ServiceHealthStatus.Maintenance => "Maintenance", + ServiceHealthStatus.Degraded => "Degraded", + _ => "Unknown" + }; + + public static string ToColorCode(this ServiceHealthStatus status) => status switch + { + ServiceHealthStatus.Operational => "#28a745", + ServiceHealthStatus.MinorOutage => "#ffc107", + ServiceHealthStatus.MajorOutage => "#dc3545", + ServiceHealthStatus.Maintenance => "#17a2b8", + ServiceHealthStatus.Degraded => "#fd7e14", + _ => "#6c757d" + }; +} diff --git a/TaskGenix.Core/Responses/TaskGen/ResourceResponse.cs b/TaskGenix.Core/Responses/TaskGen/ResourceResponse.cs new file mode 100644 index 0000000..5ea5c68 --- /dev/null +++ b/TaskGenix.Core/Responses/TaskGen/ResourceResponse.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Core.Responses.TaskGen +{ + public class ResourceResponse + { + public Guid Id { get; set; } + public string Title { get; set; } = null!; + public string Url { get; set; } = null!; + public string? Description { get; set; } + public string Type { get; set; } = nameof(ResourceType.Other); + } +} diff --git a/TaskGenix.Core/Responses/TaskGen/TaskAssignmentResponse.cs b/TaskGenix.Core/Responses/TaskGen/TaskAssignmentResponse.cs index 6a9dbb2..3971cd4 100644 --- a/TaskGenix.Core/Responses/TaskGen/TaskAssignmentResponse.cs +++ b/TaskGenix.Core/Responses/TaskGen/TaskAssignmentResponse.cs @@ -1,10 +1,13 @@ +using System; using TaskGenix.Core.Responses.User; -namespace TaskGenix.Core.Responses.TaskGen +namespace TaskGenix.Core.Responses.TaskGen; + +public class TaskAssignmentResponse { - public class TaskAssignmentResponse - { - public UserResponse? AssignedUser { get; set; } - public DateTime AssignedAt { get; set; } - } -} + public Guid Id { get; set; } + public Guid TaskId { get; set; } + public Guid UserId { get; set; } + public UserResponse? AssignedUser { get; set; } + public DateTime AssignedAt { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/TaskGen/TaskGenResponse.cs b/TaskGenix.Core/Responses/TaskGen/TaskGenResponse.cs index 8da9a42..31b920f 100644 --- a/TaskGenix.Core/Responses/TaskGen/TaskGenResponse.cs +++ b/TaskGenix.Core/Responses/TaskGen/TaskGenResponse.cs @@ -1,17 +1,19 @@ using System; using System.Collections.Generic; -namespace TaskGenix.Core.Responses.TaskGen +namespace TaskGenix.Core.Responses.TaskGen; + +public class TaskGenResponse { - public class TaskGenResponse - { - public Guid Id { get; set; } - public string? Title { get; set; } - public string? Description { get; set; } - public DateTime? DueDate { get; set; } - public Guid? ProjectId { get; set; } - public string? Status { get; set; } - public int? Priority { get; set; } - public List Assignments { get; set; } = new(); - } -} + public Guid Id { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + public DateTime? DueDate { get; set; } + public Guid? ProjectId { get; set; } + public string? Status { get; set; } + public string? Priority { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public List Assignments { get; set; } = new(); + public List Resources { get; set; } = new(); +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/User/UserResponse.cs b/TaskGenix.Core/Responses/User/UserResponse.cs index a20441a..25195a0 100644 --- a/TaskGenix.Core/Responses/User/UserResponse.cs +++ b/TaskGenix.Core/Responses/User/UserResponse.cs @@ -1,11 +1,60 @@ using System; +using System.ComponentModel.DataAnnotations; -namespace TaskGenix.Core.Responses.User +namespace TaskGenix.Core.Responses.User; + +public class UserResponse +{ + public Guid Id { get; set; } + public string? Username { get; set; } + public string? Email { get; set; } + public string? ProfilePicture { get; set; } +} + +// For POST - Create new user (all fields required) +public class UserRequest { - public class UserResponse - { - public Guid Id { get; set; } - public string? Username { get; set; } - public string? Email { get; set; } - } + [Required(ErrorMessage = "Username is required")] + [MaxLength(100, ErrorMessage = "Username cannot exceed 100 characters")] + public string? Username { get; set; } + + [Required(ErrorMessage = "Email is required")] + [MaxLength(100, ErrorMessage = "Email cannot exceed 100 characters")] + [EmailAddress(ErrorMessage = "Please provide a valid email address")] + public string? Email { get; set; } + + [Required(ErrorMessage = "Password is required")] + [MinLength(6, ErrorMessage = "Password must be at least 6 characters long")] + public string? Password { get; set; } } + +// For PUT - Update entire user (all fields required) +public class UpdateUserRequest +{ + [Required(ErrorMessage = "Username is required")] + [MaxLength(100, ErrorMessage = "Username cannot exceed 100 characters")] + public string? Username { get; set; } + + [Required(ErrorMessage = "Email is required")] + [MaxLength(100, ErrorMessage = "Email cannot exceed 100 characters")] + [EmailAddress(ErrorMessage = "Please provide a valid email address")] + public string? Email { get; set; } + + [Required(ErrorMessage = "Password is required")] + [MinLength(6, ErrorMessage = "Password must be at least 6 characters long")] + public string? Password { get; set; } +} + +// For PATCH - Partial update (no fields required) +public class UpdatePartialUserRequest +{ + [MaxLength(100, ErrorMessage = "Username cannot exceed 100 characters")] + public string? Username { get; set; } + + [MaxLength(100, ErrorMessage = "Email cannot exceed 100 characters")] + [EmailAddress(ErrorMessage = "Please provide a valid email address")] + public string? Email { get; set; } + + [MinLength(6, ErrorMessage = "Password must be at least 6 characters long")] + public string? Password { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/UserDeviceToken/UserDeviceTokenResponses.cs b/TaskGenix.Core/Responses/UserDeviceToken/UserDeviceTokenResponses.cs new file mode 100644 index 0000000..fee3df1 --- /dev/null +++ b/TaskGenix.Core/Responses/UserDeviceToken/UserDeviceTokenResponses.cs @@ -0,0 +1,14 @@ +namespace TaskGenix.Core.Responses.UserDeviceToken; + +public class UserDeviceTokenResponse +{ + public Guid Id { get; set; } + public Guid UserId { get; set; } + public string Email { get; set; } = null!; + public string DeviceId { get; set; } = null!; + public string FCMToken { get; set; } = null!; + public string? DeviceName { get; set; } + public string Platform { get; set; } = null!; + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/UserPlan/UserPlanResponse.cs b/TaskGenix.Core/Responses/UserPlan/UserPlanResponse.cs new file mode 100644 index 0000000..6bc8d47 --- /dev/null +++ b/TaskGenix.Core/Responses/UserPlan/UserPlanResponse.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; + +namespace TaskGenix.Core.Responses.UserPlan +{ + public class UserPlanResponse + { + public Guid Id { get; set; } + + public string Name { get; set; } = null!; + + public decimal Price { get; set; } + + public Guid UserId { get; set; } + public Guid PlanId { get; set; } + public List FeatureIds { get; set; } = new List(); + + public DateTime StartDate { get; set; } + + public DateTime EndDate { get; set; } + + public bool IsActive { get; set; } + + public DateTime CreatedAt { get; set; } + + public DateTime UpdatedAt { get; set; } + } +} \ No newline at end of file diff --git a/TaskGenix.Core/Responses/UserPlanFeature/CreateUserPlanFeatureResponse.cs b/TaskGenix.Core/Responses/UserPlanFeature/CreateUserPlanFeatureResponse.cs new file mode 100644 index 0000000..91f2e55 --- /dev/null +++ b/TaskGenix.Core/Responses/UserPlanFeature/CreateUserPlanFeatureResponse.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaskGenix.Core.Responses.UserPlanFeature +{ + public class CreateUserPlanFeatureResponse + { + public Guid Id { get; set; } + + public Guid UserPlanId { get; set; } + public Guid PlanFeatureId { get; set; } + + public decimal QuotaTotal { get; set; } + public decimal QuotaUsed { get; set; } + public decimal QuotaRemaining { get; set; } + + public bool IsUnlimited { get; set; } + + // Flattened feature metadata for client convenience + public Guid? FeatureId { get; set; } + public string? FeatureKey { get; set; } + public string? FeatureName { get; set; } + public string? FeatureUnit { get; set; } + + // FeatureType as string to avoid tight coupling with domain enum in DTOs + public string? FeatureType { get; set; } + + // Plan-level configured quota value (nullable because plan feature may omit it) + public decimal? PlanFeatureQuotaValue { get; set; } + } +} diff --git a/TaskGenix.Core/Responses/UserPlanFeature/UpdateUserPlanFeatureResponse.cs b/TaskGenix.Core/Responses/UserPlanFeature/UpdateUserPlanFeatureResponse.cs new file mode 100644 index 0000000..37b071e --- /dev/null +++ b/TaskGenix.Core/Responses/UserPlanFeature/UpdateUserPlanFeatureResponse.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TaskGenix.Core.Responses.UserPlanFeature +{ + public class UpdateUserPlanFeatureResponse + { + // Indicates whether the update succeeded + public bool Updated { get; set; } + + // Updated resource representation (optional when update fails) + public CreateUserPlanFeatureResponse? Data { get; set; } + + // Optional message for errors or informational text + public string? Message { get; set; } + } +} diff --git a/TaskGenix.Core/Settings/AppSettings.cs b/TaskGenix.Core/Settings/AppSettings.cs new file mode 100644 index 0000000..abbbb41 --- /dev/null +++ b/TaskGenix.Core/Settings/AppSettings.cs @@ -0,0 +1,18 @@ +namespace TaskGenix.Core.Settings; + + +public class EmailSettings +{ + public string SmtpServer { get; set; } = string.Empty; + public int SmtpPort { get; set; } + public string FromEmail { get; set; } = string.Empty; + public string FromName { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} + +public class FirebaseCloudMessagingSettings +{ + public string ServerKey { get; set; } = string.Empty; + public string SenderId { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/TaskGenix.Core/TaskGenix.Core.csproj b/TaskGenix.Core/TaskGenix.Core.csproj index 6903198..59ed72a 100644 --- a/TaskGenix.Core/TaskGenix.Core.csproj +++ b/TaskGenix.Core/TaskGenix.Core.csproj @@ -1,33 +1,31 @@  - - net8.0 - enable - enable - + + net8.0 + enable + enable + - - - - + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - + + + diff --git a/TaskGenix.Core/Validators/ValidateFileAttribute.cs b/TaskGenix.Core/Validators/ValidateFileAttribute.cs index 16038d2..1c9e0a6 100644 --- a/TaskGenix.Core/Validators/ValidateFileAttribute.cs +++ b/TaskGenix.Core/Validators/ValidateFileAttribute.cs @@ -3,88 +3,84 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -namespace TaskGenix.Core.Extensions +namespace TaskGenix.Core.Extensions; + +public class ValidateFileAttribute : ActionFilterAttribute { - public class ValidateFileAttribute : ActionFilterAttribute + private readonly int _maxSizeInMB; + private readonly string[] _allowedExtensions; + private readonly bool _allowMultipleFiles; + private readonly string _formFieldName; + + public ValidateFileAttribute( + int maxSizeInMB = 5, + string[]? allowedExtensions = null, + bool allowMultipleFiles = false, + string formFieldName = "file") { - private readonly int _maxSizeInMB; - private readonly string[] _allowedExtensions; - private readonly bool _allowMultipleFiles; - private readonly string _formFieldName; + _maxSizeInMB = maxSizeInMB; + _allowedExtensions = allowedExtensions ?? new[] { ".jpg", ".jpeg", ".png" }; + _allowMultipleFiles = allowMultipleFiles; + _formFieldName = formFieldName; + } - public ValidateFileAttribute( - int maxSizeInMB = 5, - string[]? allowedExtensions = null, - bool allowMultipleFiles = false, - string formFieldName = "file") + public override void OnActionExecuting(ActionExecutingContext context) + { + var files = context.HttpContext.Request.Form.Files + .Where(f => f.Name.Equals(_formFieldName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (!files.Any()) { - _maxSizeInMB = maxSizeInMB; - _allowedExtensions = allowedExtensions ?? new[] { ".jpg", ".jpeg", ".png" }; - _allowMultipleFiles = allowMultipleFiles; - _formFieldName = formFieldName; + AddError(context, "No file was uploaded"); + return; } - public override void OnActionExecuting(ActionExecutingContext context) + if (!_allowMultipleFiles && files.Count > 1) { - var files = context.HttpContext.Request.Form.Files - .Where(f => f.Name.Equals(_formFieldName, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (!files.Any()) - { - AddError(context, "No file was uploaded"); - return; - } + AddError(context, "Multiple files are not allowed"); + return; + } - if (!_allowMultipleFiles && files.Count > 1) + foreach (var file in files) + { + var validationError = ValidateFile(file); + if (!string.IsNullOrEmpty(validationError)) { - AddError(context, "Multiple files are not allowed"); + AddError(context, validationError); return; } - - foreach (var file in files) - { - var validationError = ValidateFile(file); - if (!string.IsNullOrEmpty(validationError)) - { - AddError(context, validationError); - return; - } - } } + } - private string? ValidateFile(IFormFile file) - { - // Validate file extension - var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); - if (!_allowedExtensions.Contains(extension)) - { - return $"Unsupported file extension '{extension}'. Only {String.Join(", ", _allowedExtensions)} are allowed"; - } + private string? ValidateFile(IFormFile file) + { + // Validate file extension + var extension = Path.GetExtension(file.FileName).ToLowerInvariant(); + if (!_allowedExtensions.Contains(extension)) + return + $"Unsupported file extension '{extension}'. Only {string.Join(", ", _allowedExtensions)} are allowed"; - // Validate file size - if (file.Length > _maxSizeInMB * 1024 * 1024) - { - return $"The file '{file.FileName}' exceeds the maximum size of {_maxSizeInMB}MB"; - } + // Validate file size + if (file.Length > _maxSizeInMB * 1024 * 1024) + return $"The file '{file.FileName}' exceeds the maximum size of {_maxSizeInMB}MB"; - return null; - } + return null; + } - private void AddError(ActionExecutingContext context, string errorMessage) + private void AddError(ActionExecutingContext context, string errorMessage) + { + // Create an ErrorResponse using the new format + var errorResponse = new ErrorResponse { - // Create an ErrorResponse using the new format - var errorResponse = new ErrorResponse - { - Message = "File validation failed", - Reason = errorMessage, - Errors = new List { $"{_formFieldName}: {errorMessage}" } - }; + Message = "File validation failed", + Reason = errorMessage, + Errors = new List { $"{_formFieldName}: {errorMessage}" } + }; - context.Result = new ObjectResult(errorResponse) - { - StatusCode = StatusCodes.Status422UnprocessableEntity - }; - } + context.Result = new ObjectResult(errorResponse) + { + StatusCode = StatusCodes.Status422UnprocessableEntity + }; } -} +} \ No newline at end of file diff --git a/TaskGenix.Core/Validators/ValidateModelAttribute.cs b/TaskGenix.Core/Validators/ValidateModelAttribute.cs index deaae81..e881dd6 100644 --- a/TaskGenix.Core/Validators/ValidateModelAttribute.cs +++ b/TaskGenix.Core/Validators/ValidateModelAttribute.cs @@ -4,99 +4,97 @@ using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.ModelBinding; -namespace TaskGenix.Core.Validators +namespace TaskGenix.Core.Validators; + +public class ValidateModelAttribute : ActionFilterAttribute { - public class ValidateModelAttribute : ActionFilterAttribute + // This class is used to validate the model attributes + // If the model is not valid, it will return a bad request response + public override void OnActionExecuting(ActionExecutingContext context) { - // This class is used to validate the model attributes - // If the model is not valid, it will return a bad request response - public override void OnActionExecuting(ActionExecutingContext context) + if (!context.ModelState.IsValid) { - if (!context.ModelState.IsValid) - { - var errorsList = new List(); - var detailedErrorsDictionary = new Dictionary(); + var errorsList = new List(); + var detailedErrorsDictionary = new Dictionary(); - // Process model state errors - foreach (var modelStateEntry in context.ModelState) - { - var key = modelStateEntry.Key; - var errorMessages = new List(); + // Process model state errors + foreach (var modelStateEntry in context.ModelState) + { + var key = modelStateEntry.Key; + var errorMessages = new List(); - foreach (var error in modelStateEntry.Value.Errors) + foreach (var error in modelStateEntry.Value.Errors) + if (!string.IsNullOrEmpty(error.ErrorMessage)) + { + errorMessages.Add(error.ErrorMessage); + } + else if (error.Exception != null) { - if (!string.IsNullOrEmpty(error.ErrorMessage)) + // Check for specific exceptions related to invalid data types + if (error.Exception is FormatException || error.Exception is InvalidCastException) { - errorMessages.Add(error.ErrorMessage); + var propertyName = key.Split('.').Last(); + var customMessage = GetCustomTypeErrorMessage(propertyName, context.ActionArguments); + errorMessages.Add(customMessage); } - else if (error.Exception != null) + else { - // Check for specific exceptions related to invalid data types - if (error.Exception is FormatException || error.Exception is InvalidCastException) - { - string propertyName = key.Split('.').Last(); - string customMessage = GetCustomTypeErrorMessage(propertyName, context.ActionArguments); - errorMessages.Add(customMessage); - } - else - { - errorMessages.Add(error.Exception.Message); - } + errorMessages.Add(error.Exception.Message); } } - // Add default required field message if no errors were added but the field is invalid - if (!errorMessages.Any() && modelStateEntry.Value.ValidationState == ModelValidationState.Invalid) - { - string propertyName = key.Split('.').Last(); - errorMessages.Add($"The {propertyName} field is required."); - } - - if (errorMessages.Any()) - { - // Add to the flat list for ErrorResponse.Errors - errorMessages.ForEach(msg => errorsList.Add($"{key}: {msg}")); - - // Also keep the structured format for Data - detailedErrorsDictionary[key] = errorMessages.ToArray(); - } + // Add default required field message if no errors were added but the field is invalid + if (!errorMessages.Any() && modelStateEntry.Value.ValidationState == ModelValidationState.Invalid) + { + var propertyName = key.Split('.').Last(); + errorMessages.Add($"The {propertyName} field is required."); } - // Create ErrorResponse using the new format - var errorResponse = new ErrorResponse + if (errorMessages.Any()) { - Message = "Validation failed", - Reason = "One or more model validation errors occurred", - Errors = errorsList - }; + // Add to the flat list for ErrorResponse.Errors + errorMessages.ForEach(msg => errorsList.Add($"{key}: {msg}")); - context.Result = new ObjectResult(errorResponse) - { - StatusCode = StatusCodes.Status422UnprocessableEntity - }; + // Also keep the structured format for Data + detailedErrorsDictionary[key] = errorMessages.ToArray(); + } } + + // Create ErrorResponse using the new format + var errorResponse = new ErrorResponse + { + Message = "Validation failed", + Reason = "One or more model validation errors occurred", + Errors = errorsList + }; + + context.Result = new ObjectResult(errorResponse) + { + StatusCode = StatusCodes.Status422UnprocessableEntity + }; } + } - private string GetCustomTypeErrorMessage(string propertyName, IDictionary actionArguments) + private string GetCustomTypeErrorMessage(string propertyName, IDictionary actionArguments) + { + // Try to get the expected type from the action parameters + var parameter = + actionArguments.FirstOrDefault(p => p.Key.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); + if (parameter.Value != null) { - // Try to get the expected type from the action parameters - var parameter = actionArguments.FirstOrDefault(p => p.Key.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); - if (parameter.Value != null) + var expectedType = parameter.Value.GetType(); + return expectedType.Name switch { - Type expectedType = parameter.Value.GetType(); - return expectedType.Name switch - { - nameof(Int32) or nameof(Int64) or nameof(Double) or nameof(Decimal) - => $"The field {propertyName} must be a valid number.", - nameof(DateTime) => $"The field {propertyName} must be a valid date and time.", - nameof(Boolean) => $"The field {propertyName} must be true or false.", - nameof(Guid) => $"The field {propertyName} must be a valid GUID.", - _ when expectedType.IsEnum => $"The field {propertyName} must be one of the allowed values.", - _ => $"The field {propertyName} has an invalid value." - }; - } - - return $"The field {propertyName} has an invalid value."; + nameof(Int32) or nameof(Int64) or nameof(Double) or nameof(Decimal) + => $"The field {propertyName} must be a valid number.", + nameof(DateTime) => $"The field {propertyName} must be a valid date and time.", + nameof(Boolean) => $"The field {propertyName} must be true or false.", + nameof(Guid) => $"The field {propertyName} must be a valid GUID.", + _ when expectedType.IsEnum => $"The field {propertyName} must be one of the allowed values.", + _ => $"The field {propertyName} has an invalid value." + }; } + + return $"The field {propertyName} has an invalid value."; } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/AttachmentConfiguration.cs b/TaskGenix.DAL/Data/Configurations/AttachmentConfiguration.cs index a42f6f8..a277d77 100644 --- a/TaskGenix.DAL/Data/Configurations/AttachmentConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/AttachmentConfiguration.cs @@ -2,25 +2,24 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class AttachmentConfiguration : BaseEntityConfiguration { - public class AttachmentConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - // Call the base configuration first to set up common properties - base.Configure(builder); + // Call the base configuration first to set up common properties + base.Configure(builder); - // TaskGen - Attachment relationship - builder.HasOne(a => a.TaskGen) - .WithMany(t => t.Attachments) - .HasForeignKey(a => a.TaskGenId) - .OnDelete(DeleteBehavior.Cascade); + // TaskGen - Attachment relationship + builder.HasOne(a => a.TaskGen) + .WithMany(t => t.Attachments) + .HasForeignKey(a => a.TaskGenId) + .OnDelete(DeleteBehavior.Cascade); - // Indexing - builder.HasIndex(a => a.TaskGenId); - builder.HasIndex(a => a.FileType); - } + // Indexing + builder.HasIndex(a => a.TaskGenId); + builder.HasIndex(a => a.FileType); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/BaseEntityConfiguration.cs b/TaskGenix.DAL/Data/Configurations/BaseEntityConfiguration.cs index c03a4bd..ee62142 100644 --- a/TaskGenix.DAL/Data/Configurations/BaseEntityConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/BaseEntityConfiguration.cs @@ -2,19 +2,18 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public abstract class BaseEntityConfiguration : IEntityTypeConfiguration + where TEntity : BaseEntity { - public abstract class BaseEntityConfiguration : IEntityTypeConfiguration - where TEntity : BaseEntity + public virtual void Configure(EntityTypeBuilder builder) { - public virtual void Configure(EntityTypeBuilder builder) - { - // Configure timestamp properties for all entities that inherit from BaseEntity - builder.Property(e => e.CreatedAt) - .HasColumnType("timestamp without time zone"); + // Configure timestamp properties for all entities that inherit from BaseEntity + builder.Property(e => e.CreatedAt) + .HasColumnType("timestamp with time zone"); - builder.Property(e => e.UpdatedAt) - .HasColumnType("timestamp without time zone"); - } + builder.Property(e => e.UpdatedAt) + .HasColumnType("timestamp with time zone"); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/FeatureConfiguration.cs b/TaskGenix.DAL/Data/Configurations/FeatureConfiguration.cs index 675b67b..d57b49c 100644 --- a/TaskGenix.DAL/Data/Configurations/FeatureConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/FeatureConfiguration.cs @@ -2,19 +2,18 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class FeatureConfiguration : BaseEntityConfiguration { - public class FeatureConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - // Call the base configuration first to set up common properties - base.Configure(builder); + // Call the base configuration first to set up common properties + base.Configure(builder); - builder.Property(f => f.FeatureType) - .HasConversion(); + builder.Property(f => f.FeatureType) + .HasConversion(); - builder.HasIndex(f => f.Key).IsUnique(); - } + builder.HasIndex(f => f.Key).IsUnique(); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/JwtTokenConfiguration.cs b/TaskGenix.DAL/Data/Configurations/JwtTokenConfiguration.cs new file mode 100644 index 0000000..7460ea1 --- /dev/null +++ b/TaskGenix.DAL/Data/Configurations/JwtTokenConfiguration.cs @@ -0,0 +1,68 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TaskGenix.Core.Domains; + +namespace TaskGenix.DAL.Data.Configurations; + +public class JwtTokenConfiguration : BaseEntityConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder.ToTable("JwtTokens"); + + builder.HasKey(t => t.Id); + + builder.Property(t => t.AccessToken) + .IsRequired() + .HasMaxLength(500); + + builder.Property(t => t.RefreshToken) + .IsRequired() + .HasMaxLength(500); + + builder.Property(t => t.ExpirationDate) + .IsRequired() + .HasColumnType("timestamp with time zone"); + + builder.Property(t => t.RefreshTokenExpirationDate) + .IsRequired() + .HasColumnType("timestamp with time zone"); + + builder.Property(t => t.IsMobile) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(t => t.IsRevoked) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(t => t.IsExpired) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(t => t.DeviceInfo) + .HasMaxLength(100); + + builder.Property(t => t.IpAddress) + .HasMaxLength(45); + + builder.Property(t => t.UserId) + .IsRequired(); + + // Foreign key relationship + builder.HasOne(t => t.User) + .WithMany(u => u.JwtTokens) + .HasForeignKey(t => t.UserId) + .OnDelete(DeleteBehavior.Cascade); + + // Indexes for performance + builder.HasIndex(t => t.RefreshToken) + .IsUnique(); + + builder.HasIndex(t => t.UserId); + + builder.HasIndex(t => new { t.UserId, t.IsRevoked }); + + builder.HasIndex(t => t.RefreshTokenExpirationDate); + } +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/NotificationConfiguration.cs b/TaskGenix.DAL/Data/Configurations/NotificationConfiguration.cs index d735e40..49a8999 100644 --- a/TaskGenix.DAL/Data/Configurations/NotificationConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/NotificationConfiguration.cs @@ -11,12 +11,6 @@ public override void Configure(EntityTypeBuilder builder) // Call the base configuration first to set up common properties base.Configure(builder); { - // User - Notification relationship - builder.HasOne(n => n.User) - .WithMany(u => u.Notifications) - .HasForeignKey(n => n.UserId) - .OnDelete(DeleteBehavior.Cascade); - // TaskGen - Notification relationship (optional) builder.HasOne(n => n.TaskGen) .WithMany(t => t.Notifications) @@ -31,22 +25,31 @@ public override void Configure(EntityTypeBuilder builder) builder.Property(n => n.IsRead) .HasDefaultValue(false); - // enum storage as string - builder.Property(n => n.DeliveryChannel) - .HasConversion(); builder.Property(n => n.ReminderPattern) - .HasConversion(); + .HasConversion() + .HasDefaultValue(ReminderPattern.ONCE); builder.Property(n => n.Type) .HasConversion() - .HasDefaultValue(NotificationType.Information); + .HasDefaultValue(NotificationType.INFO); builder.Property(n => n.DeliveryStatus) - .HasConversion(); + .HasConversion() + .HasDefaultValue(DeliveryStatus.PENDING); + + builder.Property(n => n.Icon) + .HasConversion() + .HasDefaultValue(NotificationIcon.MESSAGE); + + builder.Property(n => n.Color) + .HasConversion() + .HasDefaultValue(NotificationColor.BLUE); + + builder.Property(n => n.Email) + .IsRequired(); - // Indexing - builder.HasIndex(n => n.UserId); + builder.HasIndex(n => n.Email); builder.HasIndex(n => n.IsRead); builder.HasIndex(n => n.IsSent); builder.HasIndex(n => n.ScheduledTime); diff --git a/TaskGenix.DAL/Data/Configurations/OtpCodeConfiguration.cs b/TaskGenix.DAL/Data/Configurations/OtpCodeConfiguration.cs new file mode 100644 index 0000000..ff70744 --- /dev/null +++ b/TaskGenix.DAL/Data/Configurations/OtpCodeConfiguration.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using TaskGenix.Core.Domains; + +namespace TaskGenix.DAL.Data.Configurations; + +public class OtpCodeConfiguration : BaseEntityConfiguration +{ + public override void Configure(EntityTypeBuilder builder) + { + builder.ToTable("OtpCodes"); + + builder.HasKey(o => o.Id); + + builder.Property(o => o.Email) + .IsRequired() + .HasMaxLength(100); + + builder.Property(o => o.Code) + .IsRequired() + .HasMaxLength(10); + + builder.Property(o => o.ExpirationDate) + .IsRequired() + .HasColumnType("timestamp with time zone"); + + builder.Property(o => o.IsUsed) + .IsRequired() + .HasDefaultValue(false); + + builder.Property(o => o.Purpose) + .IsRequired() + .HasConversion(); + + builder.Property(o => o.AttemptCount) + .IsRequired() + .HasDefaultValue(0); + + builder.Property(o => o.IpAddress) + .HasMaxLength(45); + + // Computed property for IsExpired + builder.Ignore(o => o.IsExpired); + + // Indexes for performance + builder.HasIndex(o => new { o.Email, o.Purpose, o.IsUsed }); + + builder.HasIndex(o => o.ExpirationDate); + + builder.HasIndex(o => o.CreatedAt); + } +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/PaymentTransactionConfiguration.cs b/TaskGenix.DAL/Data/Configurations/PaymentTransactionConfiguration.cs index a0de1dd..e284cec 100644 --- a/TaskGenix.DAL/Data/Configurations/PaymentTransactionConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/PaymentTransactionConfiguration.cs @@ -2,44 +2,43 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class PaymentTransactionConfiguration : BaseEntityConfiguration { - public class PaymentTransactionConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) + base.Configure(builder); { - base.Configure(builder); - { - // Required fields - builder.Property(pt => pt.Amount) - .HasColumnType("decimal(18,2)") - .IsRequired(); + // Required fields + builder.Property(pt => pt.Amount) + .HasColumnType("decimal(18,2)") + .IsRequired(); - builder.Property(pt => pt.Status) - .HasConversion(); + builder.Property(pt => pt.Status) + .HasConversion(); - // User - PaymentTransaction relationship - builder.HasOne(pt => pt.User) - .WithMany(u => u.PaymentTransactions) - .HasForeignKey(pt => pt.UserId) - .OnDelete(DeleteBehavior.Restrict); + // User - PaymentTransaction relationship + builder.HasOne(pt => pt.User) + .WithMany(u => u.PaymentTransactions) + .HasForeignKey(pt => pt.UserId) + .OnDelete(DeleteBehavior.Restrict); - // UserPlan - PaymentTransaction relationship - builder.HasOne(pt => pt.UserPlan) - .WithMany() - .HasForeignKey(pt => pt.UserPlanId) - .OnDelete(DeleteBehavior.Restrict); + // UserPlan - PaymentTransaction relationship + builder.HasOne(pt => pt.UserPlan) + .WithMany() + .HasForeignKey(pt => pt.UserPlanId) + .OnDelete(DeleteBehavior.Restrict); - builder.Property(pt => pt.TransactionDate) - .HasColumnType("timestamp without time zone"); + builder.Property(pt => pt.TransactionDate) + .HasColumnType("timestamp with time zone"); - // Indexing - builder.HasIndex(pt => pt.UserId); - builder.HasIndex(pt => pt.UserPlanId); - builder.HasIndex(pt => pt.Status); - builder.HasIndex(pt => pt.TransactionDate); - builder.HasIndex(pt => pt.ExternalTransactionId); - } + // Indexing + builder.HasIndex(pt => pt.UserId); + builder.HasIndex(pt => pt.UserPlanId); + builder.HasIndex(pt => pt.Status); + builder.HasIndex(pt => pt.TransactionDate); + builder.HasIndex(pt => pt.ExternalTransactionId); } } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/PlanConfiguration.cs b/TaskGenix.DAL/Data/Configurations/PlanConfiguration.cs index dd08807..6fb8f7a 100644 --- a/TaskGenix.DAL/Data/Configurations/PlanConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/PlanConfiguration.cs @@ -2,20 +2,19 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class PlanConfiguration : BaseEntityConfiguration { - public class PlanConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - // Call the base configuration first to set up common properties - base.Configure(builder); + // Call the base configuration first to set up common properties + base.Configure(builder); - builder.Property(p => p.Price) - .HasColumnType("decimal(18,2)"); + builder.Property(p => p.Price) + .HasColumnType("decimal(18,2)"); - // Indexing - builder.HasIndex(p => p.Price); - } + // Indexing + builder.HasIndex(p => p.Price); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/PlanFeatureConfiguration.cs b/TaskGenix.DAL/Data/Configurations/PlanFeatureConfiguration.cs index d76bb3d..40d3d09 100644 --- a/TaskGenix.DAL/Data/Configurations/PlanFeatureConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/PlanFeatureConfiguration.cs @@ -2,31 +2,29 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class PlanFeatureConfiguration : BaseEntityConfiguration { - public class PlanFeatureConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) + base.Configure(builder); { - base.Configure(builder); - { - - // PlanFeature -> Plan - builder.HasOne(pf => pf.Plan) - .WithMany(p => p.Features) - .HasForeignKey(pf => pf.PlanId) - .OnDelete(DeleteBehavior.Cascade); + // PlanFeature -> Plan + builder.HasOne(pf => pf.Plan) + .WithMany(p => p.Features) + .HasForeignKey(pf => pf.PlanId) + .OnDelete(DeleteBehavior.Cascade); - // PlanFeature -> Feature - builder.HasOne(pf => pf.Feature) - .WithMany(f => f.PlanFeatures) - .HasForeignKey(pf => pf.FeatureId) - .OnDelete(DeleteBehavior.Restrict); + // PlanFeature -> Feature + builder.HasOne(pf => pf.Feature) + .WithMany(f => f.PlanFeatures) + .HasForeignKey(pf => pf.FeatureId) + .OnDelete(DeleteBehavior.Restrict); - // Indexing - each feature should be set only once per plan - builder.HasIndex(pf => new { pf.PlanId, pf.FeatureId }) - .IsUnique(); - } + // Indexing - each feature should be set only once per plan + builder.HasIndex(pf => new { pf.PlanId, pf.FeatureId }) + .IsUnique(); } } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/ProjectConfiguration.cs b/TaskGenix.DAL/Data/Configurations/ProjectConfiguration.cs index 91038c0..880797f 100644 --- a/TaskGenix.DAL/Data/Configurations/ProjectConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/ProjectConfiguration.cs @@ -2,31 +2,31 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class ProjectConfiguration : BaseEntityConfiguration { - public class ProjectConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) + base.Configure(builder); { - base.Configure(builder); - { - // Enum conversions - builder.Property(p => p.Status) - .HasConversion(); + // Enum conversions + builder.Property(p => p.Status) + .HasConversion(); - builder.Property(p => p.Type) - .HasConversion(); + builder.Property(p => p.Type) + .HasConversion(); - // Project - User (Many-to-One) for Owner - builder.HasOne(p => p.Owner) - .WithMany(u => u.OwnedProjects) - .HasForeignKey(p => p.OwnerId) - .OnDelete(DeleteBehavior.Restrict); + // Project - User (Many-to-One) for Owner + builder.HasOne(p => p.Owner) + .WithMany(u => u.OwnedProjects) + .HasForeignKey(p => p.OwnerId) + .OnDelete(DeleteBehavior.Restrict); - // Indexing - builder.HasIndex(p => p.OwnerId); - builder.HasIndex(p => p.Name); - } + // Indexing + builder.HasIndex(p => p.OwnerId); + builder.HasIndex(p => p.Name); + builder.HasIndex(p => p.IsActive); } } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/ProjectMemberConfiguration.cs b/TaskGenix.DAL/Data/Configurations/ProjectMemberConfiguration.cs index 38c888d..b71f25a 100644 --- a/TaskGenix.DAL/Data/Configurations/ProjectMemberConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/ProjectMemberConfiguration.cs @@ -2,37 +2,36 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class ProjectMemberConfiguration : BaseEntityConfiguration { - public class ProjectMemberConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - base.Configure(builder); - - builder.Property(pm => pm.Role) - .HasConversion(); - - // User - ProjectMember relationship (1 - M) - builder.HasOne(pm => pm.User) - .WithMany(u => u.ProjectMemberships) - .HasForeignKey(pm => pm.UserId) - .OnDelete(DeleteBehavior.Restrict); - - // Project - ProjectMember relationship (1 - M) - builder.HasOne(pm => pm.Project) - .WithMany(p => p.Members) - .HasForeignKey(pm => pm.ProjectId) - .OnDelete(DeleteBehavior.Cascade); - - builder.Property(pm => pm.JoinedAt) - .HasColumnType("timestamp without time zone"); - - // Indexing - user should be a member of a project only once - builder.HasIndex(pm => new { pm.ProjectId, pm.UserId }) - .IsUnique(); - - builder.HasIndex(pm => pm.Role); - } + base.Configure(builder); + + builder.Property(pm => pm.Role) + .HasConversion(); + + // User - ProjectMember relationship (1 - M) + builder.HasOne(pm => pm.User) + .WithMany(u => u.ProjectMemberships) + .HasForeignKey(pm => pm.UserId) + .OnDelete(DeleteBehavior.Restrict); + + // Project - ProjectMember relationship (1 - M) + builder.HasOne(pm => pm.Project) + .WithMany(p => p.Members) + .HasForeignKey(pm => pm.ProjectId) + .OnDelete(DeleteBehavior.Cascade); + + builder.Property(pm => pm.JoinedAt) + .HasColumnType("timestamp with time zone"); + + // Indexing - user should be a member of a project only once + builder.HasIndex(pm => new { pm.ProjectId, pm.UserId }) + .IsUnique(); + + builder.HasIndex(pm => pm.Role); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/RecordingConfiguration.cs b/TaskGenix.DAL/Data/Configurations/RecordingConfiguration.cs index 22851fa..bef0008 100644 --- a/TaskGenix.DAL/Data/Configurations/RecordingConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/RecordingConfiguration.cs @@ -2,23 +2,22 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class RecordingConfiguration : BaseEntityConfiguration { - public class RecordingConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - // User - Recording relationship - builder.HasOne(r => r.Owner) - .WithMany() - .HasForeignKey(r => r.UserId) - .OnDelete(DeleteBehavior.Restrict); + // Project - Recording relationship + builder.HasOne(r => r.Project) + .WithOne(p => p.Recording) + .HasForeignKey(r => r.ProjectId) + .OnDelete(DeleteBehavior.Cascade); - // Project - Recording relationship - builder.HasOne(r => r.Project) - .WithOne(p => p.Recording) - .HasForeignKey(r => r.ProjectId) - .OnDelete(DeleteBehavior.Cascade); - } + // Owner - Recording relationship + builder.HasOne(r => r.Owner) + .WithMany(u => u.Recordings) + .HasForeignKey(r => r.OwnerId) + .OnDelete(DeleteBehavior.SetNull); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/ResourceLinkConfiguration.cs b/TaskGenix.DAL/Data/Configurations/ResourceLinkConfiguration.cs index fe91081..3249d8d 100644 --- a/TaskGenix.DAL/Data/Configurations/ResourceLinkConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/ResourceLinkConfiguration.cs @@ -2,26 +2,25 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class ResourceLinkConfiguration : BaseEntityConfiguration { - public class ResourceLinkConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - base.Configure(builder); + base.Configure(builder); - builder.Property(r => r.Type) - .HasConversion(); + builder.Property(r => r.Type) + .HasConversion(); - // TaskGen - ResourceLink relationship - builder.HasOne(r => r.TaskGen) - .WithMany(t => t.ResourceLinks) - .HasForeignKey(r => r.TaskGenId) - .OnDelete(DeleteBehavior.Cascade); + // TaskGen - ResourceLink relationship + builder.HasOne(r => r.TaskGen) + .WithMany(t => t.ResourceLinks) + .HasForeignKey(r => r.TaskGenId) + .OnDelete(DeleteBehavior.Cascade); - // Indexing - builder.HasIndex(r => r.TaskGenId); - builder.HasIndex(r => r.Type); - } + // Indexing + builder.HasIndex(r => r.TaskGenId); + builder.HasIndex(r => r.Type); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/TaskAssignmentConfiguration.cs b/TaskGenix.DAL/Data/Configurations/TaskAssignmentConfiguration.cs index c599547..6006e99 100644 --- a/TaskGenix.DAL/Data/Configurations/TaskAssignmentConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/TaskAssignmentConfiguration.cs @@ -2,32 +2,30 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class TaskAssignmentConfiguration : IEntityTypeConfiguration { - public class TaskAssignmentConfiguration : IEntityTypeConfiguration + public void Configure(EntityTypeBuilder builder) { - public void Configure(EntityTypeBuilder builder) - { - - // TaskGen - TaskAssignment (One-to-Many) - builder.HasOne(ta => ta.TaskGen) - .WithMany(t => t.Assignments) - .HasForeignKey(ta => ta.TaskId) - .OnDelete(DeleteBehavior.Cascade); + // TaskGen - TaskAssignment (One-to-Many) + builder.HasOne(ta => ta.TaskGen) + .WithMany(t => t.Assignments) + .HasForeignKey(ta => ta.TaskId) + .OnDelete(DeleteBehavior.Cascade); - // User - TaskAssignment (One-to-Many) - builder.HasOne(ta => ta.AssignedUser) - .WithMany(u => u.TaskAssignments) - .HasForeignKey(ta => ta.UserId) - .OnDelete(DeleteBehavior.Restrict); + // User - TaskAssignment (One-to-Many) + builder.HasOne(ta => ta.AssignedUser) + .WithMany(u => u.TaskAssignments) + .HasForeignKey(ta => ta.UserId) + .OnDelete(DeleteBehavior.Restrict); - // Default value for assignment date - builder.Property(ta => ta.AssignedAt) - .HasColumnType("timestamp without time zone"); + // Default value for assignment date + builder.Property(ta => ta.AssignedAt) + .HasColumnType("timestamp with time zone"); - // Indexing - builder.HasIndex(ta => new { ta.TaskId, ta.UserId }) - .IsUnique(); - } + // Indexing + builder.HasIndex(ta => new { ta.TaskId, ta.UserId }) + .IsUnique(); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/TaskGenConfiguration.cs b/TaskGenix.DAL/Data/Configurations/TaskGenConfiguration.cs index 8663ff2..41111bc 100644 --- a/TaskGenix.DAL/Data/Configurations/TaskGenConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/TaskGenConfiguration.cs @@ -2,35 +2,33 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class TaskGenConfiguration : BaseEntityConfiguration { - public class TaskGenConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) + // Call the base configuration first to set up common properties + base.Configure(builder); { - // Call the base configuration first to set up common properties - base.Configure(builder); - { - - // Enum conversions - builder.Property(t => t.Status) - .HasConversion(); + // Enum conversions + builder.Property(t => t.Status) + .HasConversion(); - builder.Property(t => t.Priority) - .HasConversion(); + builder.Property(t => t.Priority) + .HasConversion(); - // Project - TaskGen (One-to-Many) - builder.HasOne(t => t.Project) - .WithMany(p => p.TaskGens) - .HasForeignKey(t => t.ProjectId) - .OnDelete(DeleteBehavior.Cascade); + // Project - TaskGen (One-to-Many) + builder.HasOne(t => t.Project) + .WithMany(p => p.TaskGens) + .HasForeignKey(t => t.ProjectId) + .OnDelete(DeleteBehavior.Cascade); - // Indexing - builder.HasIndex(t => t.ProjectId); - builder.HasIndex(t => t.DueDate); - builder.HasIndex(t => t.Status); - } + // Indexing + builder.HasIndex(t => t.ProjectId); + builder.HasIndex(t => t.DueDate); + builder.HasIndex(t => t.Status); } } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/TaskReminderConfigConfiguration.cs b/TaskGenix.DAL/Data/Configurations/TaskReminderConfigConfiguration.cs index 19c770c..aef3096 100644 --- a/TaskGenix.DAL/Data/Configurations/TaskReminderConfigConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/TaskReminderConfigConfiguration.cs @@ -2,22 +2,18 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class TaskReminderConfigConfiguration : BaseEntityConfiguration { - public class TaskReminderConfigConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - base.Configure(builder); - - builder.Property(rc => rc.PreferredChannel) - .HasConversion(); + base.Configure(builder); - // TaskGen - TaskReminderConfig relationship - builder.HasOne(rc => rc.TaskGen) - .WithOne(t => t.ReminderConfig) - .HasForeignKey(rc => rc.TaskGenId) - .OnDelete(DeleteBehavior.Cascade); - } + // TaskGen - TaskReminderConfig relationship + builder.HasOne(rc => rc.TaskGen) + .WithOne(t => t.ReminderConfig) + .HasForeignKey(rc => rc.TaskGenId) + .OnDelete(DeleteBehavior.Cascade); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/UserConfiguration.cs b/TaskGenix.DAL/Data/Configurations/UserConfiguration.cs index 0297327..7078a29 100644 --- a/TaskGenix.DAL/Data/Configurations/UserConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/UserConfiguration.cs @@ -2,22 +2,21 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class UserConfiguration : BaseEntityConfiguration { - public class UserConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - // Call the base configuration first to set up common properties - base.Configure(builder); + // Call the base configuration first to set up common properties + base.Configure(builder); - // Enum conversion for UserRole - builder.Property(u => u.Role) - .HasConversion(); + // Enum conversion for UserRole + builder.Property(u => u.Role) + .HasConversion(); - // Indexes - builder.HasIndex(u => u.Email).IsUnique(); - builder.HasIndex(u => u.Username).IsUnique(); - } + // Indexes + builder.HasIndex(u => u.Email).IsUnique(); + builder.HasIndex(u => u.Username).IsUnique(); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/UserDeviceTokenConfiguration.cs b/TaskGenix.DAL/Data/Configurations/UserDeviceTokenConfiguration.cs index 95dba4a..2fed875 100644 --- a/TaskGenix.DAL/Data/Configurations/UserDeviceTokenConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/UserDeviceTokenConfiguration.cs @@ -2,26 +2,19 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class UserDeviceTokenConfiguration : BaseEntityConfiguration { - public class UserDeviceTokenConfiguration : BaseEntityConfiguration - { - public override void Configure(EntityTypeBuilder builder) - { - // User - UserDeviceToken relationship - builder.HasOne(dt => dt.User) - .WithMany(u => u.DeviceTokens) - .HasForeignKey(dt => dt.UserId) - .OnDelete(DeleteBehavior.Cascade); - // Indexing - builder.HasIndex(dt => dt.UserId); - builder.HasIndex(dt => dt.DeviceId); - builder.HasIndex(dt => dt.Platform); + public override void Configure(EntityTypeBuilder builder) + { + builder.HasIndex(dt => dt.DeviceId); + builder.HasIndex(dt => dt.Platform); + builder.HasIndex(dt => dt.Email); - // Unique token per device - builder.HasIndex(dt => new { dt.DeviceId, dt.Platform }) - .IsUnique(); - } + // Unique token per device + builder.HasIndex(dt => new { dt.DeviceId, dt.Platform }) + .IsUnique(); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/UserPlanConfiguration.cs b/TaskGenix.DAL/Data/Configurations/UserPlanConfiguration.cs index 9c608bf..856e5fc 100644 --- a/TaskGenix.DAL/Data/Configurations/UserPlanConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/UserPlanConfiguration.cs @@ -2,32 +2,31 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class UserPlanConfiguration : BaseEntityConfiguration { - public class UserPlanConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - // Call the base configuration first to set up common properties - base.Configure(builder); + // Call the base configuration first to set up common properties + base.Configure(builder); - // User - UserPlan relationship - builder.HasOne(up => up.User) - .WithMany(u => u.UserPlans) - .HasForeignKey(up => up.UserId) - .OnDelete(DeleteBehavior.Cascade); + // User - UserPlan relationship + builder.HasOne(up => up.User) + .WithMany(u => u.UserPlans) + .HasForeignKey(up => up.UserId) + .OnDelete(DeleteBehavior.Cascade); - // Plan - UserPlan relationship - builder.HasOne(up => up.Plan) - .WithMany(p => p.UserPlans) - .HasForeignKey(up => up.PlanId) - .OnDelete(DeleteBehavior.Restrict); + // Plan - UserPlan relationship + builder.HasOne(up => up.Plan) + .WithMany(p => p.UserPlans) + .HasForeignKey(up => up.PlanId) + .OnDelete(DeleteBehavior.Restrict); - // Indexing - builder.HasIndex(up => up.UserId); - builder.HasIndex(up => up.IsActive); - builder.HasIndex(up => up.EndDate); - } + // Indexing + builder.HasIndex(up => up.UserId); + builder.HasIndex(up => up.IsActive); + builder.HasIndex(up => up.EndDate); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/UserPlanFeatureConfiguration.cs b/TaskGenix.DAL/Data/Configurations/UserPlanFeatureConfiguration.cs index 0af26bb..740f66d 100644 --- a/TaskGenix.DAL/Data/Configurations/UserPlanFeatureConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/UserPlanFeatureConfiguration.cs @@ -2,43 +2,45 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class UserPlanFeatureConfiguration : BaseEntityConfiguration { - public class UserPlanFeatureConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - base.Configure(builder); + base.Configure(builder); + + builder.Property(upf => upf.QuotaTotal) + .HasColumnType("decimal(18,2)") + .IsRequired(false); - builder.Property(upf => upf.QuotaTotal) - .HasColumnType("decimal(18,2)"); + builder.Property(upf => upf.QuotaUsed) + .HasColumnType("decimal(18,2)") + .IsRequired(false); - builder.Property(upf => upf.QuotaUsed) - .HasColumnType("decimal(18,2)") - .HasDefaultValue(0); - builder.Property(upf => upf.QuotaRemaining) - .HasColumnType("decimal(18,2)"); + builder.Property(upf => upf.QuotaRemaining) + .HasColumnType("decimal(18,2)") + .IsRequired(false); - builder.Property(upf => upf.IsUnlimited) - .HasDefaultValue(false); + builder.Property(upf => upf.IsUnlimited) + .HasDefaultValue(false); - // UserPlan - UserPlanFeature relationship - builder.HasOne(upf => upf.UserPlan) - .WithMany(up => up.Features) - .HasForeignKey(upf => upf.UserPlanId) - .OnDelete(DeleteBehavior.Cascade); + // UserPlan - UserPlanFeature relationship + builder.HasOne(upf => upf.UserPlan) + .WithMany(up => up.Features) + .HasForeignKey(upf => upf.UserPlanId) + .OnDelete(DeleteBehavior.Cascade); - // Feature - UserPlanFeature relationship - builder.HasOne(upf => upf.Feature) - .WithMany() - .HasForeignKey(upf => upf.FeatureId) - .OnDelete(DeleteBehavior.Restrict); + // Feature - UserPlanFeature relationship + builder.HasOne(upf => upf.PlanFeature) + .WithMany() + .HasForeignKey(upf => upf.PlanFeatureId) + .OnDelete(DeleteBehavior.Restrict); - // Indexing - each feature should appear only once per user plan - builder.HasIndex(upf => new { upf.UserPlanId, upf.FeatureId }) - .IsUnique(); - } + // Indexing - each feature should appear only once per user plan + builder.HasIndex(upf => new { upf.UserPlanId, upf.PlanFeatureId }) + .IsUnique(); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/Configurations/UserSettingConfiguration.cs b/TaskGenix.DAL/Data/Configurations/UserSettingConfiguration.cs index d0af184..a9dd4bb 100644 --- a/TaskGenix.DAL/Data/Configurations/UserSettingConfiguration.cs +++ b/TaskGenix.DAL/Data/Configurations/UserSettingConfiguration.cs @@ -2,25 +2,23 @@ using Microsoft.EntityFrameworkCore.Metadata.Builders; using TaskGenix.Core.Domains; -namespace TaskGenix.DAL.Data.Configurations +namespace TaskGenix.DAL.Data.Configurations; + +public class UserSettingConfiguration : BaseEntityConfiguration { - public class UserSettingConfiguration : BaseEntityConfiguration + public override void Configure(EntityTypeBuilder builder) { - public override void Configure(EntityTypeBuilder builder) - { - base.Configure(builder); - - builder.HasOne(us => us.User) - .WithOne(u => u.Settings) - .HasForeignKey(us => us.UserId) - .OnDelete(DeleteBehavior.Cascade); + base.Configure(builder); - builder.Property(us => us.DarkModeEnabled) - .HasDefaultValue(false); + builder.HasOne(us => us.User) + .WithOne(u => u.Settings) + .HasForeignKey(us => us.UserId) + .OnDelete(DeleteBehavior.Cascade); - builder.Property(us => us.ReceiveEmailNotifications) - .HasDefaultValue(true); + builder.Property(us => us.DarkModeEnabled) + .HasDefaultValue(false); - } + builder.Property(us => us.ReceiveEmailNotifications) + .HasDefaultValue(true); } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/TaskGenixDbContext.cs b/TaskGenix.DAL/Data/TaskGenixDbContext.cs index 60029bc..02bc1e9 100644 --- a/TaskGenix.DAL/Data/TaskGenixDbContext.cs +++ b/TaskGenix.DAL/Data/TaskGenixDbContext.cs @@ -6,76 +6,74 @@ using TaskGenix.Core.Domains; using TaskGenix.DAL.Data.Configurations; -namespace TaskGenix.DAL.Data +namespace TaskGenix.DAL.Data; + +public class TaskGenixDbContext : DbContext { - public class TaskGenixDbContext : DbContext + public TaskGenixDbContext(DbContextOptions options) : base(options) { - public TaskGenixDbContext(DbContextOptions options) : base(options) - { - } + } - // Main entities - public DbSet Users { get; set; } - public DbSet Projects { get; set; } - public DbSet Tasks { get; set; } - public DbSet Plans { get; set; } + // Main entities + public DbSet Users { get; set; } + public DbSet Projects { get; set; } + public DbSet Tasks { get; set; } + public DbSet Plans { get; set; } - // Dependent entities - public DbSet UserSettings { get; set; } - public DbSet Attachments { get; set; } - public DbSet ResourceLinks { get; set; } - public DbSet Recordings { get; set; } - public DbSet Notifications { get; set; } - public DbSet TaskReminderConfigs { get; set; } - public DbSet UserDeviceTokens { get; set; } - public DbSet Features { get; set; } + // Dependent entities + public DbSet UserSettings { get; set; } + public DbSet Attachments { get; set; } + public DbSet ResourceLinks { get; set; } + public DbSet Recordings { get; set; } + public DbSet Notifications { get; set; } + public DbSet TaskReminderConfigs { get; set; } + public DbSet UserDeviceTokens { get; set; } + public DbSet Features { get; set; } + public DbSet JwtTokens { get; set; } + public DbSet OtpCodes { get; set; } - // Junction entities - public DbSet ProjectMembers { get; set; } - public DbSet TaskAssignments { get; set; } - public DbSet PlanFeatures { get; set; } - public DbSet UserPlans { get; set; } - public DbSet UserPlanFeatures { get; set; } - public DbSet PaymentTransactions { get; set; } + // Junction entities + public DbSet ProjectMembers { get; set; } + public DbSet TaskAssignments { get; set; } + public DbSet PlanFeatures { get; set; } + public DbSet UserPlans { get; set; } + public DbSet UserPlanFeatures { get; set; } + public DbSet PaymentTransactions { get; set; } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - modelBuilder.HasDefaultSchema("TaskGenixDb"); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.HasDefaultSchema("TaskGenixDb"); - modelBuilder.ApplyConfigurationsFromAssembly(typeof(TaskGenixDbContext).Assembly); - } + modelBuilder.ApplyConfigurationsFromAssembly(typeof(TaskGenixDbContext).Assembly); + } - public override int SaveChanges() - { - UpdateTimestamps(); - return base.SaveChanges(); - } + public override int SaveChanges() + { + UpdateTimestamps(); + return base.SaveChanges(); + } - public override Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - UpdateTimestamps(); - return base.SaveChangesAsync(cancellationToken); - } + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + UpdateTimestamps(); + return base.SaveChangesAsync(cancellationToken); + } - private void UpdateTimestamps() - { - var entries = ChangeTracker - .Entries() - .Where(e => e.Entity is BaseEntity && (e.State == EntityState.Added || e.State == EntityState.Modified)); + private void UpdateTimestamps() + { + var entries = ChangeTracker + .Entries() + .Where(e => e.Entity is BaseEntity && (e.State == EntityState.Added || e.State == EntityState.Modified)); - foreach (var entityEntry in entries) - { - var now = DateTime.UtcNow; - var entity = (BaseEntity)entityEntry.Entity; + foreach (var entityEntry in entries) + { + var now = DateTime.UtcNow; + var entity = (BaseEntity)entityEntry.Entity; - if (entityEntry.State == EntityState.Added) - { - entity.CreatedAt = now; - } + if (entityEntry.State == EntityState.Added) entity.CreatedAt = now; - entity.UpdatedAt = now; - } + entity.UpdatedAt = now; } } -} +} \ No newline at end of file diff --git a/TaskGenix.DAL/Data/TaskGenixDbContextFactory.cs b/TaskGenix.DAL/Data/TaskGenixDbContextFactory.cs new file mode 100644 index 0000000..9652359 --- /dev/null +++ b/TaskGenix.DAL/Data/TaskGenixDbContextFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; +using System.IO; + +namespace TaskGenix.DAL.Data; + +public class TaskGenixDbContextFactory : IDesignTimeDbContextFactory +{ + public TaskGenixDbContext CreateDbContext(string[] args) + { + // Đọc configuration từ Server project + var configurationBuilder = new ConfigurationBuilder() + .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../TaskGenix.Server")) + .AddJsonFile("appsettings.json", false) + .AddJsonFile("appsettings.Development.json", true); + + var configuration = configurationBuilder.Build(); + + var optionsBuilder = new DbContextOptionsBuilder(); + var connectionString = configuration.GetConnectionString("SupabaseConnection"); + + optionsBuilder.UseNpgsql(connectionString); + + return new TaskGenixDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/TaskGenix.DAL/Migrations/20250909150810_InitDb_v2.cs b/TaskGenix.DAL/Migrations/20250909150810_InitDb_v2.cs deleted file mode 100644 index f7b2e99..0000000 --- a/TaskGenix.DAL/Migrations/20250909150810_InitDb_v2.cs +++ /dev/null @@ -1,888 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace TaskGenix.DAL.Migrations -{ - /// - public partial class InitDb_v2 : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "TaskGenixDb"); - - migrationBuilder.CreateTable( - name: "Features", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Key = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Unit = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - FeatureType = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Features", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Plans", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - DurationInDays = table.Column(type: "integer", nullable: false), - Price = table.Column(type: "numeric(18,2)", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Plans", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "Users", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Username = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - AvatarUrl = table.Column(type: "text", nullable: true), - Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - PasswordHash = table.Column(type: "text", nullable: false), - Role = table.Column(type: "text", nullable: false), - LastLogin = table.Column(type: "timestamp with time zone", nullable: true), - EmailVerified = table.Column(type: "boolean", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "PlanFeatures", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - QuotaValue = table.Column(type: "numeric", nullable: true), - IsUnlimited = table.Column(type: "boolean", nullable: false), - PlanId = table.Column(type: "uuid", nullable: false), - FeatureId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PlanFeatures", x => x.Id); - table.ForeignKey( - name: "FK_PlanFeatures_Features_FeatureId", - column: x => x.FeatureId, - principalSchema: "TaskGenixDb", - principalTable: "Features", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_PlanFeatures_Plans_PlanId", - column: x => x.PlanId, - principalSchema: "TaskGenixDb", - principalTable: "Plans", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Projects", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), - Status = table.Column(type: "text", nullable: false), - Type = table.Column(type: "text", nullable: false), - OwnerId = table.Column(type: "uuid", nullable: false), - RecordingId = table.Column(type: "uuid", nullable: true), - DueDate = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Projects", x => x.Id); - table.ForeignKey( - name: "FK_Projects_Users_OwnerId", - column: x => x.OwnerId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "UserDeviceTokens", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - FCMToken = table.Column(type: "text", nullable: false), - DeviceId = table.Column(type: "text", nullable: false), - DeviceName = table.Column(type: "text", nullable: false), - Platform = table.Column(type: "text", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserDeviceTokens", x => x.Id); - table.ForeignKey( - name: "FK_UserDeviceTokens_Users_UserId", - column: x => x.UserId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserPlans", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "text", nullable: false), - Price = table.Column(type: "numeric", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - PlanId = table.Column(type: "uuid", nullable: false), - StartDate = table.Column(type: "timestamp with time zone", nullable: false), - EndDate = table.Column(type: "timestamp with time zone", nullable: false), - IsActive = table.Column(type: "boolean", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserPlans", x => x.Id); - table.ForeignKey( - name: "FK_UserPlans_Plans_PlanId", - column: x => x.PlanId, - principalSchema: "TaskGenixDb", - principalTable: "Plans", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_UserPlans_Users_UserId", - column: x => x.UserId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "UserSettings", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - ReceiveEmailNotifications = table.Column(type: "boolean", nullable: false, defaultValue: true), - DarkModeEnabled = table.Column(type: "boolean", nullable: false, defaultValue: false), - Language = table.Column(type: "text", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserSettings", x => x.Id); - table.ForeignKey( - name: "FK_UserSettings_Users_UserId", - column: x => x.UserId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ProjectMembers", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Role = table.Column(type: "text", nullable: false), - ProjectId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - JoinedAt = table.Column(type: "timestamp without time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ProjectMembers", x => x.Id); - table.ForeignKey( - name: "FK_ProjectMembers_Projects_ProjectId", - column: x => x.ProjectId, - principalSchema: "TaskGenixDb", - principalTable: "Projects", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ProjectMembers_Users_UserId", - column: x => x.UserId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Recordings", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - FileUrl = table.Column(type: "text", nullable: false), - Duration = table.Column(type: "interval", nullable: false), - ProjectId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Recordings", x => x.Id); - table.ForeignKey( - name: "FK_Recordings_Projects_ProjectId", - column: x => x.ProjectId, - principalSchema: "TaskGenixDb", - principalTable: "Projects", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Recordings_Users_UserId", - column: x => x.UserId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "TaskGens", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), - Status = table.Column(type: "text", nullable: false), - Priority = table.Column(type: "text", nullable: false), - ProjectId = table.Column(type: "uuid", nullable: false), - DueDate = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TaskGens", x => x.Id); - table.ForeignKey( - name: "FK_TaskGens_Projects_ProjectId", - column: x => x.ProjectId, - principalSchema: "TaskGenixDb", - principalTable: "Projects", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "PaymentTransactions", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Amount = table.Column(type: "numeric(18,2)", nullable: false), - Currency = table.Column(type: "character varying(3)", maxLength: 3, nullable: false), - TransactionDate = table.Column(type: "timestamp without time zone", nullable: false), - PaymentMethod = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - PaymentGateway = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), - ExternalTransactionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - Status = table.Column(type: "text", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - UserPlanId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_PaymentTransactions", x => x.Id); - table.ForeignKey( - name: "FK_PaymentTransactions_UserPlans_UserPlanId", - column: x => x.UserPlanId, - principalSchema: "TaskGenixDb", - principalTable: "UserPlans", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_PaymentTransactions_Users_UserId", - column: x => x.UserId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "UserPlanFeatures", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - QuotaTotal = table.Column(type: "numeric(18,2)", nullable: false), - QuotaUsed = table.Column(type: "numeric(18,2)", nullable: false, defaultValue: 0m), - QuotaRemaining = table.Column(type: "numeric(18,2)", nullable: false), - IsUnlimited = table.Column(type: "boolean", nullable: false, defaultValue: false), - UserPlanId = table.Column(type: "uuid", nullable: false), - FeatureId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserPlanFeatures", x => x.Id); - table.ForeignKey( - name: "FK_UserPlanFeatures_Features_FeatureId", - column: x => x.FeatureId, - principalSchema: "TaskGenixDb", - principalTable: "Features", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_UserPlanFeatures_UserPlans_UserPlanId", - column: x => x.UserPlanId, - principalSchema: "TaskGenixDb", - principalTable: "UserPlans", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Attachments", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - FileName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - FileType = table.Column(type: "text", nullable: false), - FileSize = table.Column(type: "bigint", nullable: false), - Data = table.Column(type: "bytea", nullable: false), - TaskGenId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Attachments", x => x.Id); - table.ForeignKey( - name: "FK_Attachments_TaskGens_TaskGenId", - column: x => x.TaskGenId, - principalSchema: "TaskGenixDb", - principalTable: "TaskGens", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Notifications", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Message = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), - Type = table.Column(type: "text", nullable: false, defaultValue: "Information"), - IsSent = table.Column(type: "boolean", nullable: false, defaultValue: false), - IsRead = table.Column(type: "boolean", nullable: false, defaultValue: false), - TaskGenId = table.Column(type: "uuid", nullable: true), - SequenceNumber = table.Column(type: "integer", nullable: true), - ReminderPattern = table.Column(type: "text", nullable: true), - ReminderSeriesId = table.Column(type: "uuid", nullable: true), - ScheduledTime = table.Column(type: "timestamp with time zone", nullable: true), - SentAt = table.Column(type: "timestamp with time zone", nullable: true), - DeliveryStatus = table.Column(type: "text", nullable: true), - DeliveryChannel = table.Column(type: "text", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Notifications", x => x.Id); - table.ForeignKey( - name: "FK_Notifications_TaskGens_TaskGenId", - column: x => x.TaskGenId, - principalSchema: "TaskGenixDb", - principalTable: "TaskGens", - principalColumn: "Id", - onDelete: ReferentialAction.SetNull); - table.ForeignKey( - name: "FK_Notifications_Users_UserId", - column: x => x.UserId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "ResourceLinks", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Url = table.Column(type: "text", nullable: false), - Description = table.Column(type: "text", nullable: true), - Type = table.Column(type: "text", nullable: false), - TaskGenId = table.Column(type: "uuid", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_ResourceLinks", x => x.Id); - table.ForeignKey( - name: "FK_ResourceLinks_TaskGens_TaskGenId", - column: x => x.TaskGenId, - principalSchema: "TaskGenixDb", - principalTable: "TaskGens", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "TaskAssignments", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - TaskId = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - AssignedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TaskAssignments", x => x.Id); - table.ForeignKey( - name: "FK_TaskAssignments_TaskGens_TaskId", - column: x => x.TaskId, - principalSchema: "TaskGenixDb", - principalTable: "TaskGens", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_TaskAssignments_Users_UserId", - column: x => x.UserId, - principalSchema: "TaskGenixDb", - principalTable: "Users", - principalColumn: "Id", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "TaskReminderConfigs", - schema: "TaskGenixDb", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - TaskGenId = table.Column(type: "uuid", nullable: false), - EnableReminders = table.Column(type: "boolean", nullable: false), - ReminderFrequencyInDays = table.Column(type: "integer", nullable: false), - StartRemindingDaysBeforeDue = table.Column(type: "integer", nullable: false), - ReminderIntervalInHours = table.Column(type: "integer", nullable: false), - PreferredReminderHour = table.Column(type: "integer", nullable: false), - EscalateReminderPriority = table.Column(type: "boolean", nullable: false), - PreferredChannel = table.Column(type: "text", nullable: false), - CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp without time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TaskReminderConfigs", x => x.Id); - table.ForeignKey( - name: "FK_TaskReminderConfigs_TaskGens_TaskGenId", - column: x => x.TaskGenId, - principalSchema: "TaskGenixDb", - principalTable: "TaskGens", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_Attachments_FileType", - schema: "TaskGenixDb", - table: "Attachments", - column: "FileType"); - - migrationBuilder.CreateIndex( - name: "IX_Attachments_TaskGenId", - schema: "TaskGenixDb", - table: "Attachments", - column: "TaskGenId"); - - migrationBuilder.CreateIndex( - name: "IX_Features_Key", - schema: "TaskGenixDb", - table: "Features", - column: "Key", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Notifications_IsRead", - schema: "TaskGenixDb", - table: "Notifications", - column: "IsRead"); - - migrationBuilder.CreateIndex( - name: "IX_Notifications_IsSent", - schema: "TaskGenixDb", - table: "Notifications", - column: "IsSent"); - - migrationBuilder.CreateIndex( - name: "IX_Notifications_ScheduledTime", - schema: "TaskGenixDb", - table: "Notifications", - column: "ScheduledTime"); - - migrationBuilder.CreateIndex( - name: "IX_Notifications_TaskGenId", - schema: "TaskGenixDb", - table: "Notifications", - column: "TaskGenId"); - - migrationBuilder.CreateIndex( - name: "IX_Notifications_UserId", - schema: "TaskGenixDb", - table: "Notifications", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_PaymentTransactions_ExternalTransactionId", - schema: "TaskGenixDb", - table: "PaymentTransactions", - column: "ExternalTransactionId"); - - migrationBuilder.CreateIndex( - name: "IX_PaymentTransactions_Status", - schema: "TaskGenixDb", - table: "PaymentTransactions", - column: "Status"); - - migrationBuilder.CreateIndex( - name: "IX_PaymentTransactions_TransactionDate", - schema: "TaskGenixDb", - table: "PaymentTransactions", - column: "TransactionDate"); - - migrationBuilder.CreateIndex( - name: "IX_PaymentTransactions_UserId", - schema: "TaskGenixDb", - table: "PaymentTransactions", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_PaymentTransactions_UserPlanId", - schema: "TaskGenixDb", - table: "PaymentTransactions", - column: "UserPlanId"); - - migrationBuilder.CreateIndex( - name: "IX_PlanFeatures_FeatureId", - schema: "TaskGenixDb", - table: "PlanFeatures", - column: "FeatureId"); - - migrationBuilder.CreateIndex( - name: "IX_PlanFeatures_PlanId_FeatureId", - schema: "TaskGenixDb", - table: "PlanFeatures", - columns: new[] { "PlanId", "FeatureId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Plans_Price", - schema: "TaskGenixDb", - table: "Plans", - column: "Price"); - - migrationBuilder.CreateIndex( - name: "IX_ProjectMembers_ProjectId_UserId", - schema: "TaskGenixDb", - table: "ProjectMembers", - columns: new[] { "ProjectId", "UserId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_ProjectMembers_Role", - schema: "TaskGenixDb", - table: "ProjectMembers", - column: "Role"); - - migrationBuilder.CreateIndex( - name: "IX_ProjectMembers_UserId", - schema: "TaskGenixDb", - table: "ProjectMembers", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_Projects_Name", - schema: "TaskGenixDb", - table: "Projects", - column: "Name"); - - migrationBuilder.CreateIndex( - name: "IX_Projects_OwnerId", - schema: "TaskGenixDb", - table: "Projects", - column: "OwnerId"); - - migrationBuilder.CreateIndex( - name: "IX_Recordings_ProjectId", - schema: "TaskGenixDb", - table: "Recordings", - column: "ProjectId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Recordings_UserId", - schema: "TaskGenixDb", - table: "Recordings", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_ResourceLinks_TaskGenId", - schema: "TaskGenixDb", - table: "ResourceLinks", - column: "TaskGenId"); - - migrationBuilder.CreateIndex( - name: "IX_ResourceLinks_Type", - schema: "TaskGenixDb", - table: "ResourceLinks", - column: "Type"); - - migrationBuilder.CreateIndex( - name: "IX_TaskAssignments_TaskId_UserId", - schema: "TaskGenixDb", - table: "TaskAssignments", - columns: new[] { "TaskId", "UserId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_TaskAssignments_UserId", - schema: "TaskGenixDb", - table: "TaskAssignments", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_TaskGens_DueDate", - schema: "TaskGenixDb", - table: "TaskGens", - column: "DueDate"); - - migrationBuilder.CreateIndex( - name: "IX_TaskGens_ProjectId", - schema: "TaskGenixDb", - table: "TaskGens", - column: "ProjectId"); - - migrationBuilder.CreateIndex( - name: "IX_TaskGens_Status", - schema: "TaskGenixDb", - table: "TaskGens", - column: "Status"); - - migrationBuilder.CreateIndex( - name: "IX_TaskReminderConfigs_TaskGenId", - schema: "TaskGenixDb", - table: "TaskReminderConfigs", - column: "TaskGenId", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_UserDeviceTokens_DeviceId", - schema: "TaskGenixDb", - table: "UserDeviceTokens", - column: "DeviceId"); - - migrationBuilder.CreateIndex( - name: "IX_UserDeviceTokens_DeviceId_Platform", - schema: "TaskGenixDb", - table: "UserDeviceTokens", - columns: new[] { "DeviceId", "Platform" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_UserDeviceTokens_Platform", - schema: "TaskGenixDb", - table: "UserDeviceTokens", - column: "Platform"); - - migrationBuilder.CreateIndex( - name: "IX_UserDeviceTokens_UserId", - schema: "TaskGenixDb", - table: "UserDeviceTokens", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_UserPlanFeatures_FeatureId", - schema: "TaskGenixDb", - table: "UserPlanFeatures", - column: "FeatureId"); - - migrationBuilder.CreateIndex( - name: "IX_UserPlanFeatures_UserPlanId_FeatureId", - schema: "TaskGenixDb", - table: "UserPlanFeatures", - columns: new[] { "UserPlanId", "FeatureId" }, - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_UserPlans_EndDate", - schema: "TaskGenixDb", - table: "UserPlans", - column: "EndDate"); - - migrationBuilder.CreateIndex( - name: "IX_UserPlans_IsActive", - schema: "TaskGenixDb", - table: "UserPlans", - column: "IsActive"); - - migrationBuilder.CreateIndex( - name: "IX_UserPlans_PlanId", - schema: "TaskGenixDb", - table: "UserPlans", - column: "PlanId"); - - migrationBuilder.CreateIndex( - name: "IX_UserPlans_UserId", - schema: "TaskGenixDb", - table: "UserPlans", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_Users_Email", - schema: "TaskGenixDb", - table: "Users", - column: "Email", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Users_Username", - schema: "TaskGenixDb", - table: "Users", - column: "Username", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_UserSettings_UserId", - schema: "TaskGenixDb", - table: "UserSettings", - column: "UserId", - unique: true); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "Attachments", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "Notifications", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "PaymentTransactions", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "PlanFeatures", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "ProjectMembers", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "Recordings", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "ResourceLinks", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "TaskAssignments", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "TaskReminderConfigs", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "UserDeviceTokens", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "UserPlanFeatures", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "UserSettings", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "TaskGens", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "Features", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "UserPlans", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "Projects", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "Plans", - schema: "TaskGenixDb"); - - migrationBuilder.DropTable( - name: "Users", - schema: "TaskGenixDb"); - } - } -} diff --git a/TaskGenix.DAL/Migrations/20250909150810_InitDb_v2.Designer.cs b/TaskGenix.DAL/Migrations/20250928164450_InitDb_v10.Designer.cs similarity index 78% rename from TaskGenix.DAL/Migrations/20250909150810_InitDb_v2.Designer.cs rename to TaskGenix.DAL/Migrations/20250928164450_InitDb_v10.Designer.cs index dd9bc95..16408e1 100644 --- a/TaskGenix.DAL/Migrations/20250909150810_InitDb_v2.Designer.cs +++ b/TaskGenix.DAL/Migrations/20250928164450_InitDb_v10.Designer.cs @@ -12,8 +12,8 @@ namespace TaskGenix.DAL.Migrations { [DbContext(typeof(TaskGenixDbContext))] - [Migration("20250909150810_InitDb_v2")] - partial class InitDb_v2 + [Migration("20250928164450_InitDb_v10")] + partial class InitDb_v10 { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -21,7 +21,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("TaskGenixDb") - .HasAnnotation("ProductVersion", "8.0.19") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -33,7 +33,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Data") .IsRequired() @@ -55,7 +55,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -73,7 +73,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("FeatureType") .IsRequired() @@ -95,7 +95,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -105,22 +105,102 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Features", "TaskGenixDb"); }); - modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsExpired") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsMobile") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); - b.Property("DeliveryChannel") + b.Property("RefreshToken") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RefreshTokenExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RefreshToken") + .IsUnique(); + + b.HasIndex("RefreshTokenExpirationDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("JwtTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("BLUE"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); b.Property("DeliveryStatus") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("PENDING"); + + b.Property("Email") + .IsRequired() .HasColumnType("text"); + b.Property("Icon") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("MESSAGE"); + b.Property("IsRead") .ValueGeneratedOnAdd() .HasColumnType("boolean") @@ -137,7 +217,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("character varying(1000)"); b.Property("ReminderPattern") - .HasColumnType("text"); + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("ONCE"); b.Property("ReminderSeriesId") .HasColumnType("uuid"); @@ -163,16 +245,18 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .ValueGeneratedOnAdd() .HasColumnType("text") - .HasDefaultValue("Information"); + .HasDefaultValue("INFO"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UserId") .HasColumnType("uuid"); b.HasKey("Id"); + b.HasIndex("Email"); + b.HasIndex("IsRead"); b.HasIndex("IsSent"); @@ -186,6 +270,60 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("Notifications", "TaskGenixDb"); }); + modelBuilder.Entity("TaskGenix.Core.Domains.OtpCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("Email", "Purpose", "IsUsed"); + + b.ToTable("OtpCodes", "TaskGenixDb"); + }); + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => { b.Property("Id") @@ -196,7 +334,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("decimal(18,2)"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Currency") .IsRequired() @@ -221,10 +359,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("TransactionDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); @@ -254,7 +392,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Description") .IsRequired() @@ -273,7 +411,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("decimal(18,2)"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -289,7 +427,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("FeatureId") .HasColumnType("uuid"); @@ -304,7 +442,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("numeric"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -323,7 +461,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Description") .IsRequired() @@ -353,7 +491,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -371,10 +509,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("JoinedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("ProjectId") .HasColumnType("uuid"); @@ -384,7 +522,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); @@ -407,17 +545,38 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); b.Property("Duration") .HasColumnType("interval"); - b.Property("FileUrl") + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsTranscriptRequested") + .HasColumnType("boolean"); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ObjectKey") .IsRequired() .HasColumnType("text"); - b.Property("ProjectId") + b.Property("OwnerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ProjectId") .HasColumnType("uuid"); b.Property("Title") @@ -425,19 +584,28 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("TranscriptErrorMessage") + .HasColumnType("text"); + + b.Property("TranscriptStatus") + .HasColumnType("integer"); + + b.Property("TranscriptText") + .HasColumnType("text"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") - .HasColumnType("uuid"); + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("OwnerId"); + b.HasIndex("ProjectId") .IsUnique(); - b.HasIndex("UserId"); - b.ToTable("Recordings", "TaskGenixDb"); }); @@ -448,7 +616,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Description") .HasColumnType("text"); @@ -466,7 +634,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Url") .IsRequired() @@ -488,7 +656,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("AssignedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("TaskId") .HasColumnType("uuid"); @@ -513,7 +681,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Description") .IsRequired() @@ -541,7 +709,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -561,7 +729,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("EnableReminders") .HasColumnType("boolean"); @@ -569,10 +737,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("EscalateReminderPriority") .HasColumnType("boolean"); - b.Property("PreferredChannel") - .IsRequired() - .HasColumnType("text"); - b.Property("PreferredReminderHour") .HasColumnType("integer"); @@ -589,7 +753,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -606,10 +770,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("AvatarUrl") - .HasColumnType("text"); + .HasMaxLength(150) + .HasColumnType("character varying(150)"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Email") .IsRequired() @@ -634,7 +799,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Username") .IsRequired() @@ -669,6 +834,10 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + b.Property("FCMToken") .IsRequired() .HasColumnType("text"); @@ -680,13 +849,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UserId") .HasColumnType("uuid"); b.HasKey("Id"); b.HasIndex("DeviceId"); + b.HasIndex("Email"); + b.HasIndex("Platform"); b.HasIndex("UserId"); @@ -704,7 +875,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("EndDate") .HasColumnType("timestamp with time zone"); @@ -726,7 +897,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); @@ -751,7 +922,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("FeatureId") .HasColumnType("uuid"); @@ -773,7 +944,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasDefaultValue(0m); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserPlanId") .HasColumnType("uuid"); @@ -795,7 +966,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("DarkModeEnabled") .ValueGeneratedOnAdd() @@ -812,7 +983,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasDefaultValue(true); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); @@ -836,6 +1007,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("TaskGen"); }); + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("JwtTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => { b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") @@ -843,15 +1025,11 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) .HasForeignKey("TaskGenId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("TaskGenix.Core.Domains.User", "User") + b.HasOne("TaskGenix.Core.Domains.User", null) .WithMany("Notifications") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("UserId"); b.Navigation("TaskGen"); - - b.Navigation("User"); }); modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => @@ -924,17 +1102,15 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("Recordings") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + b.HasOne("TaskGenix.Core.Domains.Project", "Project") .WithOne("Recording") .HasForeignKey("TaskGenix.Core.Domains.Recording", "ProjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TaskGenix.Core.Domains.User", "Owner") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + .OnDelete(DeleteBehavior.Cascade); b.Navigation("Owner"); @@ -995,13 +1171,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => { - b.HasOne("TaskGenix.Core.Domains.User", "User") + b.HasOne("TaskGenix.Core.Domains.User", null) .WithMany("DeviceTokens") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); + .HasForeignKey("UserId"); }); modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => @@ -1091,6 +1263,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { b.Navigation("DeviceTokens"); + b.Navigation("JwtTokens"); + b.Navigation("Notifications"); b.Navigation("OwnedProjects"); @@ -1099,6 +1273,8 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("ProjectMemberships"); + b.Navigation("Recordings"); + b.Navigation("Settings"); b.Navigation("TaskAssignments"); diff --git a/TaskGenix.DAL/Migrations/20250928164450_InitDb_v10.cs b/TaskGenix.DAL/Migrations/20250928164450_InitDb_v10.cs new file mode 100644 index 0000000..f3f4046 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20250928164450_InitDb_v10.cs @@ -0,0 +1,1012 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + /// + public partial class InitDb_v10 : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "TaskGenixDb"); + + //migrationBuilder.CreateTable( + // name: "Features", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Key = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + // Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + // Unit = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + // FeatureType = table.Column(type: "text", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_Features", x => x.Id); + // }); + + //migrationBuilder.CreateTable( + // name: "OtpCodes", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + // Code = table.Column(type: "character varying(10)", maxLength: 10, nullable: false), + // ExpirationDate = table.Column(type: "timestamp with time zone", nullable: false), + // IsUsed = table.Column(type: "boolean", nullable: false, defaultValue: false), + // Purpose = table.Column(type: "text", nullable: false), + // AttemptCount = table.Column(type: "integer", nullable: false, defaultValue: 0), + // IpAddress = table.Column(type: "character varying(45)", maxLength: 45, nullable: true), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_OtpCodes", x => x.Id); + // }); + + //migrationBuilder.CreateTable( + // name: "Plans", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + // Description = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + // DurationInDays = table.Column(type: "integer", nullable: false), + // Price = table.Column(type: "numeric(18,2)", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_Plans", x => x.Id); + // }); + + //migrationBuilder.CreateTable( + // name: "Users", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Username = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + // AvatarUrl = table.Column(type: "character varying(150)", maxLength: 150, nullable: true), + // Email = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + // PasswordHash = table.Column(type: "text", nullable: false), + // Role = table.Column(type: "text", nullable: false), + // LastLogin = table.Column(type: "timestamp with time zone", nullable: true), + // EmailVerified = table.Column(type: "boolean", nullable: false), + // IsActive = table.Column(type: "boolean", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_Users", x => x.Id); + // }); + + //migrationBuilder.CreateTable( + // name: "PlanFeatures", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // QuotaValue = table.Column(type: "numeric", nullable: true), + // IsUnlimited = table.Column(type: "boolean", nullable: false), + // PlanId = table.Column(type: "uuid", nullable: false), + // FeatureId = table.Column(type: "uuid", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_PlanFeatures", x => x.Id); + // table.ForeignKey( + // name: "FK_PlanFeatures_Features_FeatureId", + // column: x => x.FeatureId, + // principalSchema: "TaskGenixDb", + // principalTable: "Features", + // principalColumn: "Id", + // onDelete: ReferentialAction.Restrict); + // table.ForeignKey( + // name: "FK_PlanFeatures_Plans_PlanId", + // column: x => x.PlanId, + // principalSchema: "TaskGenixDb", + // principalTable: "Plans", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateTable( + // name: "JwtTokens", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // AccessToken = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + // RefreshToken = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), + // ExpirationDate = table.Column(type: "timestamp with time zone", nullable: false), + // RefreshTokenExpirationDate = table.Column(type: "timestamp with time zone", nullable: false), + // IsMobile = table.Column(type: "boolean", nullable: false, defaultValue: false), + // IsRevoked = table.Column(type: "boolean", nullable: false, defaultValue: false), + // IsExpired = table.Column(type: "boolean", nullable: false, defaultValue: false), + // DeviceInfo = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + // IpAddress = table.Column(type: "character varying(45)", maxLength: 45, nullable: true), + // UserId = table.Column(type: "uuid", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_JwtTokens", x => x.Id); + // table.ForeignKey( + // name: "FK_JwtTokens_Users_UserId", + // column: x => x.UserId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateTable( + // name: "Projects", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Name = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + // Description = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: false), + // Status = table.Column(type: "text", nullable: false), + // Type = table.Column(type: "text", nullable: false), + // OwnerId = table.Column(type: "uuid", nullable: false), + // RecordingId = table.Column(type: "uuid", nullable: true), + // DueDate = table.Column(type: "timestamp with time zone", nullable: true), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_Projects", x => x.Id); + // table.ForeignKey( + // name: "FK_Projects_Users_OwnerId", + // column: x => x.OwnerId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id", + // onDelete: ReferentialAction.Restrict); + // }); + + //migrationBuilder.CreateTable( + // name: "UserDeviceTokens", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // FCMToken = table.Column(type: "text", nullable: false), + // DeviceId = table.Column(type: "text", nullable: false), + // DeviceName = table.Column(type: "text", nullable: false), + // Platform = table.Column(type: "text", nullable: false), + // Email = table.Column(type: "text", nullable: false), + // UserId = table.Column(type: "uuid", nullable: true), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_UserDeviceTokens", x => x.Id); + // table.ForeignKey( + // name: "FK_UserDeviceTokens_Users_UserId", + // column: x => x.UserId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id"); + // }); + + //migrationBuilder.CreateTable( + // name: "UserPlans", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Name = table.Column(type: "text", nullable: false), + // Price = table.Column(type: "numeric", nullable: false), + // UserId = table.Column(type: "uuid", nullable: false), + // PlanId = table.Column(type: "uuid", nullable: false), + // StartDate = table.Column(type: "timestamp with time zone", nullable: false), + // EndDate = table.Column(type: "timestamp with time zone", nullable: false), + // IsActive = table.Column(type: "boolean", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_UserPlans", x => x.Id); + // table.ForeignKey( + // name: "FK_UserPlans_Plans_PlanId", + // column: x => x.PlanId, + // principalSchema: "TaskGenixDb", + // principalTable: "Plans", + // principalColumn: "Id", + // onDelete: ReferentialAction.Restrict); + // table.ForeignKey( + // name: "FK_UserPlans_Users_UserId", + // column: x => x.UserId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateTable( + // name: "UserSettings", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // ReceiveEmailNotifications = table.Column(type: "boolean", nullable: false, defaultValue: true), + // DarkModeEnabled = table.Column(type: "boolean", nullable: false, defaultValue: false), + // Language = table.Column(type: "text", nullable: false), + // UserId = table.Column(type: "uuid", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_UserSettings", x => x.Id); + // table.ForeignKey( + // name: "FK_UserSettings_Users_UserId", + // column: x => x.UserId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateTable( + // name: "ProjectMembers", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Role = table.Column(type: "text", nullable: false), + // ProjectId = table.Column(type: "uuid", nullable: false), + // UserId = table.Column(type: "uuid", nullable: false), + // JoinedAt = table.Column(type: "timestamp with time zone", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_ProjectMembers", x => x.Id); + // table.ForeignKey( + // name: "FK_ProjectMembers_Projects_ProjectId", + // column: x => x.ProjectId, + // principalSchema: "TaskGenixDb", + // principalTable: "Projects", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // table.ForeignKey( + // name: "FK_ProjectMembers_Users_UserId", + // column: x => x.UserId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id", + // onDelete: ReferentialAction.Restrict); + // }); + + migrationBuilder.CreateTable( + name: "Recordings", + schema: "TaskGenixDb", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + BucketName = table.Column(type: "text", nullable: false), + ObjectKey = table.Column(type: "text", nullable: false), + Duration = table.Column(type: "interval", nullable: false), + FileSize = table.Column(type: "bigint", nullable: false), + MimeType = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + UploadedAt = table.Column(type: "timestamp with time zone", nullable: false), + IsTranscriptRequested = table.Column(type: "boolean", nullable: false), + TranscriptStatus = table.Column(type: "integer", nullable: false), + TranscriptText = table.Column(type: "text", nullable: true), + TranscriptErrorMessage = table.Column(type: "text", nullable: true), + ProjectId = table.Column(type: "uuid", nullable: true), + OwnerEmail = table.Column(type: "text", nullable: false), + OwnerId = table.Column(type: "uuid", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Recordings", x => x.Id); + table.ForeignKey( + name: "FK_Recordings_Projects_ProjectId", + column: x => x.ProjectId, + principalSchema: "TaskGenixDb", + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Recordings_Users_OwnerId", + column: x => x.OwnerId, + principalSchema: "TaskGenixDb", + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + }); + + //migrationBuilder.CreateTable( + // name: "TaskGens", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + // Description = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), + // Status = table.Column(type: "text", nullable: false), + // Priority = table.Column(type: "text", nullable: false), + // ProjectId = table.Column(type: "uuid", nullable: false), + // DueDate = table.Column(type: "timestamp with time zone", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_TaskGens", x => x.Id); + // table.ForeignKey( + // name: "FK_TaskGens_Projects_ProjectId", + // column: x => x.ProjectId, + // principalSchema: "TaskGenixDb", + // principalTable: "Projects", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateTable( + // name: "PaymentTransactions", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Amount = table.Column(type: "numeric(18,2)", nullable: false), + // Currency = table.Column(type: "character varying(3)", maxLength: 3, nullable: false), + // TransactionDate = table.Column(type: "timestamp with time zone", nullable: false), + // PaymentMethod = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + // PaymentGateway = table.Column(type: "character varying(50)", maxLength: 50, nullable: true), + // ExternalTransactionId = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + // Status = table.Column(type: "text", nullable: false), + // UserId = table.Column(type: "uuid", nullable: false), + // UserPlanId = table.Column(type: "uuid", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_PaymentTransactions", x => x.Id); + // table.ForeignKey( + // name: "FK_PaymentTransactions_UserPlans_UserPlanId", + // column: x => x.UserPlanId, + // principalSchema: "TaskGenixDb", + // principalTable: "UserPlans", + // principalColumn: "Id", + // onDelete: ReferentialAction.Restrict); + // table.ForeignKey( + // name: "FK_PaymentTransactions_Users_UserId", + // column: x => x.UserId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id", + // onDelete: ReferentialAction.Restrict); + // }); + + //migrationBuilder.CreateTable( + // name: "UserPlanFeatures", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // QuotaTotal = table.Column(type: "numeric(18,2)", nullable: false), + // QuotaUsed = table.Column(type: "numeric(18,2)", nullable: false, defaultValue: 0m), + // QuotaRemaining = table.Column(type: "numeric(18,2)", nullable: false), + // IsUnlimited = table.Column(type: "boolean", nullable: false, defaultValue: false), + // UserPlanId = table.Column(type: "uuid", nullable: false), + // FeatureId = table.Column(type: "uuid", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_UserPlanFeatures", x => x.Id); + // table.ForeignKey( + // name: "FK_UserPlanFeatures_Features_FeatureId", + // column: x => x.FeatureId, + // principalSchema: "TaskGenixDb", + // principalTable: "Features", + // principalColumn: "Id", + // onDelete: ReferentialAction.Restrict); + // table.ForeignKey( + // name: "FK_UserPlanFeatures_UserPlans_UserPlanId", + // column: x => x.UserPlanId, + // principalSchema: "TaskGenixDb", + // principalTable: "UserPlans", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateTable( + // name: "Attachments", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // FileName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + // FileType = table.Column(type: "text", nullable: false), + // FileSize = table.Column(type: "bigint", nullable: false), + // Data = table.Column(type: "bytea", nullable: false), + // TaskGenId = table.Column(type: "uuid", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_Attachments", x => x.Id); + // table.ForeignKey( + // name: "FK_Attachments_TaskGens_TaskGenId", + // column: x => x.TaskGenId, + // principalSchema: "TaskGenixDb", + // principalTable: "TaskGens", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateTable( + // name: "Notifications", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + // Message = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: false), + // Type = table.Column(type: "text", nullable: false, defaultValue: "INFO"), + // Icon = table.Column(type: "text", nullable: true, defaultValue: "MESSAGE"), + // Color = table.Column(type: "text", nullable: true, defaultValue: "BLUE"), + // Email = table.Column(type: "text", nullable: false), + // IsSent = table.Column(type: "boolean", nullable: false, defaultValue: false), + // IsRead = table.Column(type: "boolean", nullable: false, defaultValue: false), + // TaskGenId = table.Column(type: "uuid", nullable: true), + // SequenceNumber = table.Column(type: "integer", nullable: true), + // ReminderPattern = table.Column(type: "text", nullable: true, defaultValue: "ONCE"), + // ReminderSeriesId = table.Column(type: "uuid", nullable: true), + // ScheduledTime = table.Column(type: "timestamp with time zone", nullable: true), + // SentAt = table.Column(type: "timestamp with time zone", nullable: true), + // DeliveryStatus = table.Column(type: "text", nullable: true, defaultValue: "PENDING"), + // UserId = table.Column(type: "uuid", nullable: true), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_Notifications", x => x.Id); + // table.ForeignKey( + // name: "FK_Notifications_TaskGens_TaskGenId", + // column: x => x.TaskGenId, + // principalSchema: "TaskGenixDb", + // principalTable: "TaskGens", + // principalColumn: "Id", + // onDelete: ReferentialAction.SetNull); + // table.ForeignKey( + // name: "FK_Notifications_Users_UserId", + // column: x => x.UserId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id"); + // }); + + //migrationBuilder.CreateTable( + // name: "ResourceLinks", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + // Url = table.Column(type: "text", nullable: false), + // Description = table.Column(type: "text", nullable: true), + // Type = table.Column(type: "text", nullable: false), + // TaskGenId = table.Column(type: "uuid", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_ResourceLinks", x => x.Id); + // table.ForeignKey( + // name: "FK_ResourceLinks_TaskGens_TaskGenId", + // column: x => x.TaskGenId, + // principalSchema: "TaskGenixDb", + // principalTable: "TaskGens", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateTable( + // name: "TaskAssignments", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // TaskId = table.Column(type: "uuid", nullable: false), + // UserId = table.Column(type: "uuid", nullable: false), + // AssignedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_TaskAssignments", x => x.Id); + // table.ForeignKey( + // name: "FK_TaskAssignments_TaskGens_TaskId", + // column: x => x.TaskId, + // principalSchema: "TaskGenixDb", + // principalTable: "TaskGens", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // table.ForeignKey( + // name: "FK_TaskAssignments_Users_UserId", + // column: x => x.UserId, + // principalSchema: "TaskGenixDb", + // principalTable: "Users", + // principalColumn: "Id", + // onDelete: ReferentialAction.Restrict); + // }); + + //migrationBuilder.CreateTable( + // name: "TaskReminderConfigs", + // schema: "TaskGenixDb", + // columns: table => new + // { + // Id = table.Column(type: "uuid", nullable: false), + // TaskGenId = table.Column(type: "uuid", nullable: false), + // EnableReminders = table.Column(type: "boolean", nullable: false), + // ReminderFrequencyInDays = table.Column(type: "integer", nullable: false), + // StartRemindingDaysBeforeDue = table.Column(type: "integer", nullable: false), + // ReminderIntervalInHours = table.Column(type: "integer", nullable: false), + // PreferredReminderHour = table.Column(type: "integer", nullable: false), + // EscalateReminderPriority = table.Column(type: "boolean", nullable: false), + // CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + // UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false) + // }, + // constraints: table => + // { + // table.PrimaryKey("PK_TaskReminderConfigs", x => x.Id); + // table.ForeignKey( + // name: "FK_TaskReminderConfigs_TaskGens_TaskGenId", + // column: x => x.TaskGenId, + // principalSchema: "TaskGenixDb", + // principalTable: "TaskGens", + // principalColumn: "Id", + // onDelete: ReferentialAction.Cascade); + // }); + + //migrationBuilder.CreateIndex( + // name: "IX_Attachments_FileType", + // schema: "TaskGenixDb", + // table: "Attachments", + // column: "FileType"); + + //migrationBuilder.CreateIndex( + // name: "IX_Attachments_TaskGenId", + // schema: "TaskGenixDb", + // table: "Attachments", + // column: "TaskGenId"); + + //migrationBuilder.CreateIndex( + // name: "IX_Features_Key", + // schema: "TaskGenixDb", + // table: "Features", + // column: "Key", + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_JwtTokens_RefreshToken", + // schema: "TaskGenixDb", + // table: "JwtTokens", + // column: "RefreshToken", + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_JwtTokens_RefreshTokenExpirationDate", + // schema: "TaskGenixDb", + // table: "JwtTokens", + // column: "RefreshTokenExpirationDate"); + + //migrationBuilder.CreateIndex( + // name: "IX_JwtTokens_UserId", + // schema: "TaskGenixDb", + // table: "JwtTokens", + // column: "UserId"); + + //migrationBuilder.CreateIndex( + // name: "IX_JwtTokens_UserId_IsRevoked", + // schema: "TaskGenixDb", + // table: "JwtTokens", + // columns: new[] { "UserId", "IsRevoked" }); + + //migrationBuilder.CreateIndex( + // name: "IX_Notifications_Email", + // schema: "TaskGenixDb", + // table: "Notifications", + // column: "Email"); + + //migrationBuilder.CreateIndex( + // name: "IX_Notifications_IsRead", + // schema: "TaskGenixDb", + // table: "Notifications", + // column: "IsRead"); + + //migrationBuilder.CreateIndex( + // name: "IX_Notifications_IsSent", + // schema: "TaskGenixDb", + // table: "Notifications", + // column: "IsSent"); + + //migrationBuilder.CreateIndex( + // name: "IX_Notifications_ScheduledTime", + // schema: "TaskGenixDb", + // table: "Notifications", + // column: "ScheduledTime"); + + //migrationBuilder.CreateIndex( + // name: "IX_Notifications_TaskGenId", + // schema: "TaskGenixDb", + // table: "Notifications", + // column: "TaskGenId"); + + //migrationBuilder.CreateIndex( + // name: "IX_Notifications_UserId", + // schema: "TaskGenixDb", + // table: "Notifications", + // column: "UserId"); + + //migrationBuilder.CreateIndex( + // name: "IX_OtpCodes_CreatedAt", + // schema: "TaskGenixDb", + // table: "OtpCodes", + // column: "CreatedAt"); + + //migrationBuilder.CreateIndex( + // name: "IX_OtpCodes_Email_Purpose_IsUsed", + // schema: "TaskGenixDb", + // table: "OtpCodes", + // columns: new[] { "Email", "Purpose", "IsUsed" }); + + //migrationBuilder.CreateIndex( + // name: "IX_OtpCodes_ExpirationDate", + // schema: "TaskGenixDb", + // table: "OtpCodes", + // column: "ExpirationDate"); + + //migrationBuilder.CreateIndex( + // name: "IX_PaymentTransactions_ExternalTransactionId", + // schema: "TaskGenixDb", + // table: "PaymentTransactions", + // column: "ExternalTransactionId"); + + //migrationBuilder.CreateIndex( + // name: "IX_PaymentTransactions_Status", + // schema: "TaskGenixDb", + // table: "PaymentTransactions", + // column: "Status"); + + //migrationBuilder.CreateIndex( + // name: "IX_PaymentTransactions_TransactionDate", + // schema: "TaskGenixDb", + // table: "PaymentTransactions", + // column: "TransactionDate"); + + //migrationBuilder.CreateIndex( + // name: "IX_PaymentTransactions_UserId", + // schema: "TaskGenixDb", + // table: "PaymentTransactions", + // column: "UserId"); + + //migrationBuilder.CreateIndex( + // name: "IX_PaymentTransactions_UserPlanId", + // schema: "TaskGenixDb", + // table: "PaymentTransactions", + // column: "UserPlanId"); + + //migrationBuilder.CreateIndex( + // name: "IX_PlanFeatures_FeatureId", + // schema: "TaskGenixDb", + // table: "PlanFeatures", + // column: "FeatureId"); + + //migrationBuilder.CreateIndex( + // name: "IX_PlanFeatures_PlanId_FeatureId", + // schema: "TaskGenixDb", + // table: "PlanFeatures", + // columns: new[] { "PlanId", "FeatureId" }, + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_Plans_Price", + // schema: "TaskGenixDb", + // table: "Plans", + // column: "Price"); + + //migrationBuilder.CreateIndex( + // name: "IX_ProjectMembers_ProjectId_UserId", + // schema: "TaskGenixDb", + // table: "ProjectMembers", + // columns: new[] { "ProjectId", "UserId" }, + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_ProjectMembers_Role", + // schema: "TaskGenixDb", + // table: "ProjectMembers", + // column: "Role"); + + //migrationBuilder.CreateIndex( + // name: "IX_ProjectMembers_UserId", + // schema: "TaskGenixDb", + // table: "ProjectMembers", + // column: "UserId"); + + //migrationBuilder.CreateIndex( + // name: "IX_Projects_Name", + // schema: "TaskGenixDb", + // table: "Projects", + // column: "Name"); + + //migrationBuilder.CreateIndex( + // name: "IX_Projects_OwnerId", + // schema: "TaskGenixDb", + // table: "Projects", + // column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_Recordings_OwnerId", + schema: "TaskGenixDb", + table: "Recordings", + column: "OwnerId"); + + migrationBuilder.CreateIndex( + name: "IX_Recordings_ProjectId", + schema: "TaskGenixDb", + table: "Recordings", + column: "ProjectId", + unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_ResourceLinks_TaskGenId", + // schema: "TaskGenixDb", + // table: "ResourceLinks", + // column: "TaskGenId"); + + //migrationBuilder.CreateIndex( + // name: "IX_ResourceLinks_Type", + // schema: "TaskGenixDb", + // table: "ResourceLinks", + // column: "Type"); + + //migrationBuilder.CreateIndex( + // name: "IX_TaskAssignments_TaskId_UserId", + // schema: "TaskGenixDb", + // table: "TaskAssignments", + // columns: new[] { "TaskId", "UserId" }, + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_TaskAssignments_UserId", + // schema: "TaskGenixDb", + // table: "TaskAssignments", + // column: "UserId"); + + //migrationBuilder.CreateIndex( + // name: "IX_TaskGens_DueDate", + // schema: "TaskGenixDb", + // table: "TaskGens", + // column: "DueDate"); + + //migrationBuilder.CreateIndex( + // name: "IX_TaskGens_ProjectId", + // schema: "TaskGenixDb", + // table: "TaskGens", + // column: "ProjectId"); + + //migrationBuilder.CreateIndex( + // name: "IX_TaskGens_Status", + // schema: "TaskGenixDb", + // table: "TaskGens", + // column: "Status"); + + //migrationBuilder.CreateIndex( + // name: "IX_TaskReminderConfigs_TaskGenId", + // schema: "TaskGenixDb", + // table: "TaskReminderConfigs", + // column: "TaskGenId", + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_UserDeviceTokens_DeviceId", + // schema: "TaskGenixDb", + // table: "UserDeviceTokens", + // column: "DeviceId"); + + //migrationBuilder.CreateIndex( + // name: "IX_UserDeviceTokens_DeviceId_Platform", + // schema: "TaskGenixDb", + // table: "UserDeviceTokens", + // columns: new[] { "DeviceId", "Platform" }, + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_UserDeviceTokens_Email", + // schema: "TaskGenixDb", + // table: "UserDeviceTokens", + // column: "Email"); + + //migrationBuilder.CreateIndex( + // name: "IX_UserDeviceTokens_Platform", + // schema: "TaskGenixDb", + // table: "UserDeviceTokens", + // column: "Platform"); + + //migrationBuilder.CreateIndex( + // name: "IX_UserDeviceTokens_UserId", + // schema: "TaskGenixDb", + // table: "UserDeviceTokens", + // column: "UserId"); + + //migrationBuilder.CreateIndex( + // name: "IX_UserPlanFeatures_FeatureId", + // schema: "TaskGenixDb", + // table: "UserPlanFeatures", + // column: "FeatureId"); + + //migrationBuilder.CreateIndex( + // name: "IX_UserPlanFeatures_UserPlanId_FeatureId", + // schema: "TaskGenixDb", + // table: "UserPlanFeatures", + // columns: new[] { "UserPlanId", "FeatureId" }, + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_UserPlans_EndDate", + // schema: "TaskGenixDb", + // table: "UserPlans", + // column: "EndDate"); + + //migrationBuilder.CreateIndex( + // name: "IX_UserPlans_IsActive", + // schema: "TaskGenixDb", + // table: "UserPlans", + // column: "IsActive"); + + //migrationBuilder.CreateIndex( + // name: "IX_UserPlans_PlanId", + // schema: "TaskGenixDb", + // table: "UserPlans", + // column: "PlanId"); + + //migrationBuilder.CreateIndex( + // name: "IX_UserPlans_UserId", + // schema: "TaskGenixDb", + // table: "UserPlans", + // column: "UserId"); + + //migrationBuilder.CreateIndex( + // name: "IX_Users_Email", + // schema: "TaskGenixDb", + // table: "Users", + // column: "Email", + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_Users_Username", + // schema: "TaskGenixDb", + // table: "Users", + // column: "Username", + // unique: true); + + //migrationBuilder.CreateIndex( + // name: "IX_UserSettings_UserId", + // schema: "TaskGenixDb", + // table: "UserSettings", + // column: "UserId", + // unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Attachments", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "JwtTokens", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "Notifications", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "OtpCodes", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "PaymentTransactions", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "PlanFeatures", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "ProjectMembers", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "Recordings", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "ResourceLinks", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "TaskAssignments", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "TaskReminderConfigs", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "UserDeviceTokens", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "UserPlanFeatures", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "UserSettings", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "TaskGens", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "Features", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "UserPlans", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "Projects", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "Plans", + schema: "TaskGenixDb"); + + migrationBuilder.DropTable( + name: "Users", + schema: "TaskGenixDb"); + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251004063249_addAuth0Id.Designer.cs b/TaskGenix.DAL/Migrations/20251004063249_addAuth0Id.Designer.cs new file mode 100644 index 0000000..be1ea13 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251004063249_addAuth0Id.Designer.cs @@ -0,0 +1,1295 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TaskGenix.DAL.Data; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + [DbContext(typeof(TaskGenixDbContext))] + [Migration("20251004063249_addAuth0Id")] + partial class addAuth0Id + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("TaskGenixDb") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FileType"); + + b.HasIndex("TaskGenId"); + + b.ToTable("Attachments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Features", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsExpired") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsMobile") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RefreshToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RefreshTokenExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RefreshToken") + .IsUnique(); + + b.HasIndex("RefreshTokenExpirationDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("JwtTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("BLUE"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStatus") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("PENDING"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Icon") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("MESSAGE"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsSent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ReminderPattern") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("ONCE"); + + b.Property("ReminderSeriesId") + .HasColumnType("uuid"); + + b.Property("ScheduledTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("INFO"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("IsRead"); + + b.HasIndex("IsSent"); + + b.HasIndex("ScheduledTime"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.OtpCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("Email", "Purpose", "IsUsed"); + + b.ToTable("OtpCodes", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentGateway") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransactionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExternalTransactionId"); + + b.HasIndex("Status"); + + b.HasIndex("TransactionDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserPlanId"); + + b.ToTable("PaymentTransactions", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DurationInDays") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Price"); + + b.ToTable("Plans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .HasColumnType("boolean"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("QuotaValue") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("PlanId", "FeatureId") + .IsUnique(); + + b.ToTable("PlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("RecordingId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("Projects", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Role"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectMembers", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsTranscriptRequested") + .HasColumnType("boolean"); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TranscriptErrorMessage") + .HasColumnType("text"); + + b.Property("TranscriptStatus") + .HasColumnType("integer"); + + b.Property("TranscriptText") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProjectId") + .IsUnique(); + + b.ToTable("Recordings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("Type"); + + b.ToTable("ResourceLinks", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TaskId", "UserId") + .IsUnique(); + + b.ToTable("TaskAssignments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DueDate") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DueDate"); + + b.HasIndex("ProjectId"); + + b.HasIndex("Status"); + + b.ToTable("TaskGens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnableReminders") + .HasColumnType("boolean"); + + b.Property("EscalateReminderPriority") + .HasColumnType("boolean"); + + b.Property("PreferredReminderHour") + .HasColumnType("integer"); + + b.Property("ReminderFrequencyInDays") + .HasColumnType("integer"); + + b.Property("ReminderIntervalInHours") + .HasColumnType("integer"); + + b.Property("StartRemindingDaysBeforeDue") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId") + .IsUnique(); + + b.ToTable("TaskReminderConfigs", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth0Id") + .HasColumnType("uuid"); + + b.Property("AvatarUrl") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FCMToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Email"); + + b.HasIndex("Platform"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "Platform") + .IsUnique(); + + b.ToTable("UserDeviceTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EndDate"); + + b.HasIndex("IsActive"); + + b.HasIndex("PlanId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPlans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("QuotaRemaining") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaUsed") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,2)") + .HasDefaultValue(0m); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("UserPlanId", "FeatureId") + .IsUnique(); + + b.ToTable("UserPlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DarkModeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReceiveEmailNotifications") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserSettings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Attachments") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("JwtTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Notifications") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("Notifications") + .HasForeignKey("UserId"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("PaymentTransactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany() + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany("PlanFeatures") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("Features") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("OwnedProjects") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("Members") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("Recordings") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithOne("Recording") + .HasForeignKey("TaskGenix.Core.Domains.Recording", "ProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("ResourceLinks") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Assignments") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "AssignedUser") + .WithMany("TaskAssignments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AssignedUser"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("TaskGens") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithOne("ReminderConfig") + .HasForeignKey("TaskGenix.Core.Domains.TaskReminderConfig", "TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("DeviceTokens") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("UserPlans") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("UserPlans") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plan"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany() + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany("Features") + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithOne("Settings") + .HasForeignKey("TaskGenix.Core.Domains.UserSetting", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Navigation("PlanFeatures"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Navigation("Features"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Navigation("Members"); + + b.Navigation("Recording"); + + b.Navigation("TaskGens"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Navigation("Assignments"); + + b.Navigation("Attachments"); + + b.Navigation("Notifications"); + + b.Navigation("ReminderConfig"); + + b.Navigation("ResourceLinks"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Navigation("DeviceTokens"); + + b.Navigation("JwtTokens"); + + b.Navigation("Notifications"); + + b.Navigation("OwnedProjects"); + + b.Navigation("PaymentTransactions"); + + b.Navigation("ProjectMemberships"); + + b.Navigation("Recordings"); + + b.Navigation("Settings"); + + b.Navigation("TaskAssignments"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Navigation("Features"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251004063249_addAuth0Id.cs b/TaskGenix.DAL/Migrations/20251004063249_addAuth0Id.cs new file mode 100644 index 0000000..d6033ff --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251004063249_addAuth0Id.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + /// + public partial class addAuth0Id : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Auth0Id", + schema: "TaskGenixDb", + table: "Users", + type: "uuid", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Auth0Id", + schema: "TaskGenixDb", + table: "Users"); + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251004063911_changeTypeAuth0Id.Designer.cs b/TaskGenix.DAL/Migrations/20251004063911_changeTypeAuth0Id.Designer.cs new file mode 100644 index 0000000..1ac8396 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251004063911_changeTypeAuth0Id.Designer.cs @@ -0,0 +1,1295 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TaskGenix.DAL.Data; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + [DbContext(typeof(TaskGenixDbContext))] + [Migration("20251004063911_changeTypeAuth0Id")] + partial class changeTypeAuth0Id + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("TaskGenixDb") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FileType"); + + b.HasIndex("TaskGenId"); + + b.ToTable("Attachments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Features", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsExpired") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsMobile") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RefreshToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RefreshTokenExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RefreshToken") + .IsUnique(); + + b.HasIndex("RefreshTokenExpirationDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("JwtTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("BLUE"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStatus") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("PENDING"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Icon") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("MESSAGE"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsSent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ReminderPattern") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("ONCE"); + + b.Property("ReminderSeriesId") + .HasColumnType("uuid"); + + b.Property("ScheduledTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("INFO"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("IsRead"); + + b.HasIndex("IsSent"); + + b.HasIndex("ScheduledTime"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.OtpCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("Email", "Purpose", "IsUsed"); + + b.ToTable("OtpCodes", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentGateway") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransactionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExternalTransactionId"); + + b.HasIndex("Status"); + + b.HasIndex("TransactionDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserPlanId"); + + b.ToTable("PaymentTransactions", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DurationInDays") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Price"); + + b.ToTable("Plans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .HasColumnType("boolean"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("QuotaValue") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("PlanId", "FeatureId") + .IsUnique(); + + b.ToTable("PlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("RecordingId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("Projects", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Role"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectMembers", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsTranscriptRequested") + .HasColumnType("boolean"); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TranscriptErrorMessage") + .HasColumnType("text"); + + b.Property("TranscriptStatus") + .HasColumnType("integer"); + + b.Property("TranscriptText") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProjectId") + .IsUnique(); + + b.ToTable("Recordings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("Type"); + + b.ToTable("ResourceLinks", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TaskId", "UserId") + .IsUnique(); + + b.ToTable("TaskAssignments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DueDate") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DueDate"); + + b.HasIndex("ProjectId"); + + b.HasIndex("Status"); + + b.ToTable("TaskGens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnableReminders") + .HasColumnType("boolean"); + + b.Property("EscalateReminderPriority") + .HasColumnType("boolean"); + + b.Property("PreferredReminderHour") + .HasColumnType("integer"); + + b.Property("ReminderFrequencyInDays") + .HasColumnType("integer"); + + b.Property("ReminderIntervalInHours") + .HasColumnType("integer"); + + b.Property("StartRemindingDaysBeforeDue") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId") + .IsUnique(); + + b.ToTable("TaskReminderConfigs", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth0Id") + .HasColumnType("text"); + + b.Property("AvatarUrl") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FCMToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Email"); + + b.HasIndex("Platform"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "Platform") + .IsUnique(); + + b.ToTable("UserDeviceTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EndDate"); + + b.HasIndex("IsActive"); + + b.HasIndex("PlanId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPlans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("QuotaRemaining") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaUsed") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,2)") + .HasDefaultValue(0m); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("UserPlanId", "FeatureId") + .IsUnique(); + + b.ToTable("UserPlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DarkModeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReceiveEmailNotifications") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserSettings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Attachments") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("JwtTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Notifications") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("Notifications") + .HasForeignKey("UserId"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("PaymentTransactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany() + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany("PlanFeatures") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("Features") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("OwnedProjects") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("Members") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("Recordings") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithOne("Recording") + .HasForeignKey("TaskGenix.Core.Domains.Recording", "ProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("ResourceLinks") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Assignments") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "AssignedUser") + .WithMany("TaskAssignments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AssignedUser"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("TaskGens") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithOne("ReminderConfig") + .HasForeignKey("TaskGenix.Core.Domains.TaskReminderConfig", "TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("DeviceTokens") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("UserPlans") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("UserPlans") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plan"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany() + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany("Features") + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithOne("Settings") + .HasForeignKey("TaskGenix.Core.Domains.UserSetting", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Navigation("PlanFeatures"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Navigation("Features"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Navigation("Members"); + + b.Navigation("Recording"); + + b.Navigation("TaskGens"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Navigation("Assignments"); + + b.Navigation("Attachments"); + + b.Navigation("Notifications"); + + b.Navigation("ReminderConfig"); + + b.Navigation("ResourceLinks"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Navigation("DeviceTokens"); + + b.Navigation("JwtTokens"); + + b.Navigation("Notifications"); + + b.Navigation("OwnedProjects"); + + b.Navigation("PaymentTransactions"); + + b.Navigation("ProjectMemberships"); + + b.Navigation("Recordings"); + + b.Navigation("Settings"); + + b.Navigation("TaskAssignments"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Navigation("Features"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251004063911_changeTypeAuth0Id.cs b/TaskGenix.DAL/Migrations/20251004063911_changeTypeAuth0Id.cs new file mode 100644 index 0000000..9042587 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251004063911_changeTypeAuth0Id.cs @@ -0,0 +1,39 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + /// + public partial class changeTypeAuth0Id : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Auth0Id", + schema: "TaskGenixDb", + table: "Users", + type: "text", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Auth0Id", + schema: "TaskGenixDb", + table: "Users", + type: "uuid", + nullable: true, + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251010072424_AddIsActiveProject.Designer.cs b/TaskGenix.DAL/Migrations/20251010072424_AddIsActiveProject.Designer.cs new file mode 100644 index 0000000..db4fe0a --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251010072424_AddIsActiveProject.Designer.cs @@ -0,0 +1,1300 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TaskGenix.DAL.Data; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + [DbContext(typeof(TaskGenixDbContext))] + [Migration("20251010072424_AddIsActiveProject")] + partial class AddIsActiveProject + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("TaskGenixDb") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FileType"); + + b.HasIndex("TaskGenId"); + + b.ToTable("Attachments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Features", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsExpired") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsMobile") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RefreshToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RefreshTokenExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RefreshToken") + .IsUnique(); + + b.HasIndex("RefreshTokenExpirationDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("JwtTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("BLUE"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStatus") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("PENDING"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Icon") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("MESSAGE"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsSent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ReminderPattern") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("ONCE"); + + b.Property("ReminderSeriesId") + .HasColumnType("uuid"); + + b.Property("ScheduledTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("INFO"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("IsRead"); + + b.HasIndex("IsSent"); + + b.HasIndex("ScheduledTime"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.OtpCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("Email", "Purpose", "IsUsed"); + + b.ToTable("OtpCodes", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentGateway") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransactionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExternalTransactionId"); + + b.HasIndex("Status"); + + b.HasIndex("TransactionDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserPlanId"); + + b.ToTable("PaymentTransactions", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DurationInDays") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Price"); + + b.ToTable("Plans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .HasColumnType("boolean"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("QuotaValue") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("PlanId", "FeatureId") + .IsUnique(); + + b.ToTable("PlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("RecordingId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("Projects", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Role"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectMembers", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsTranscriptRequested") + .HasColumnType("boolean"); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TranscriptErrorMessage") + .HasColumnType("text"); + + b.Property("TranscriptStatus") + .HasColumnType("integer"); + + b.Property("TranscriptText") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProjectId") + .IsUnique(); + + b.ToTable("Recordings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("Type"); + + b.ToTable("ResourceLinks", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TaskId", "UserId") + .IsUnique(); + + b.ToTable("TaskAssignments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DueDate") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DueDate"); + + b.HasIndex("ProjectId"); + + b.HasIndex("Status"); + + b.ToTable("TaskGens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnableReminders") + .HasColumnType("boolean"); + + b.Property("EscalateReminderPriority") + .HasColumnType("boolean"); + + b.Property("PreferredReminderHour") + .HasColumnType("integer"); + + b.Property("ReminderFrequencyInDays") + .HasColumnType("integer"); + + b.Property("ReminderIntervalInHours") + .HasColumnType("integer"); + + b.Property("StartRemindingDaysBeforeDue") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId") + .IsUnique(); + + b.ToTable("TaskReminderConfigs", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth0Id") + .HasColumnType("text"); + + b.Property("AvatarUrl") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FCMToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Email"); + + b.HasIndex("Platform"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "Platform") + .IsUnique(); + + b.ToTable("UserDeviceTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EndDate"); + + b.HasIndex("IsActive"); + + b.HasIndex("PlanId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPlans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("QuotaRemaining") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaUsed") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,2)") + .HasDefaultValue(0m); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("UserPlanId", "FeatureId") + .IsUnique(); + + b.ToTable("UserPlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DarkModeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReceiveEmailNotifications") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserSettings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Attachments") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("JwtTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Notifications") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("Notifications") + .HasForeignKey("UserId"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("PaymentTransactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany() + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany("PlanFeatures") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("Features") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("OwnedProjects") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("Members") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("Recordings") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithOne("Recording") + .HasForeignKey("TaskGenix.Core.Domains.Recording", "ProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("ResourceLinks") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Assignments") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "AssignedUser") + .WithMany("TaskAssignments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AssignedUser"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("TaskGens") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithOne("ReminderConfig") + .HasForeignKey("TaskGenix.Core.Domains.TaskReminderConfig", "TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("DeviceTokens") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("UserPlans") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("UserPlans") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plan"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany() + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany("Features") + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithOne("Settings") + .HasForeignKey("TaskGenix.Core.Domains.UserSetting", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Navigation("PlanFeatures"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Navigation("Features"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Navigation("Members"); + + b.Navigation("Recording"); + + b.Navigation("TaskGens"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Navigation("Assignments"); + + b.Navigation("Attachments"); + + b.Navigation("Notifications"); + + b.Navigation("ReminderConfig"); + + b.Navigation("ResourceLinks"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Navigation("DeviceTokens"); + + b.Navigation("JwtTokens"); + + b.Navigation("Notifications"); + + b.Navigation("OwnedProjects"); + + b.Navigation("PaymentTransactions"); + + b.Navigation("ProjectMemberships"); + + b.Navigation("Recordings"); + + b.Navigation("Settings"); + + b.Navigation("TaskAssignments"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Navigation("Features"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251010072424_AddIsActiveProject.cs b/TaskGenix.DAL/Migrations/20251010072424_AddIsActiveProject.cs new file mode 100644 index 0000000..5301427 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251010072424_AddIsActiveProject.cs @@ -0,0 +1,42 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + /// + public partial class AddIsActiveProject : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "IsActive", + schema: "TaskGenixDb", + table: "Projects", + type: "boolean", + nullable: false, + defaultValue: true); + + migrationBuilder.CreateIndex( + name: "IX_Projects_IsActive", + schema: "TaskGenixDb", + table: "Projects", + column: "IsActive"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Projects_IsActive", + schema: "TaskGenixDb", + table: "Projects"); + + migrationBuilder.DropColumn( + name: "IsActive", + schema: "TaskGenixDb", + table: "Projects"); + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251014162747_editForeignKey.Designer.cs b/TaskGenix.DAL/Migrations/20251014162747_editForeignKey.Designer.cs new file mode 100644 index 0000000..045204a --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251014162747_editForeignKey.Designer.cs @@ -0,0 +1,1300 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TaskGenix.DAL.Data; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + [DbContext(typeof(TaskGenixDbContext))] + [Migration("20251014162747_editForeignKey")] + partial class editForeignKey + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("TaskGenixDb") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FileType"); + + b.HasIndex("TaskGenId"); + + b.ToTable("Attachments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Features", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsExpired") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsMobile") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RefreshToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RefreshTokenExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RefreshToken") + .IsUnique(); + + b.HasIndex("RefreshTokenExpirationDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("JwtTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("BLUE"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStatus") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("PENDING"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Icon") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("MESSAGE"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsSent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ReminderPattern") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("ONCE"); + + b.Property("ReminderSeriesId") + .HasColumnType("uuid"); + + b.Property("ScheduledTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("INFO"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("IsRead"); + + b.HasIndex("IsSent"); + + b.HasIndex("ScheduledTime"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.OtpCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("Email", "Purpose", "IsUsed"); + + b.ToTable("OtpCodes", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentGateway") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransactionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExternalTransactionId"); + + b.HasIndex("Status"); + + b.HasIndex("TransactionDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserPlanId"); + + b.ToTable("PaymentTransactions", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DurationInDays") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Price"); + + b.ToTable("Plans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .HasColumnType("boolean"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("QuotaValue") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("PlanId", "FeatureId") + .IsUnique(); + + b.ToTable("PlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("RecordingId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("Projects", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Role"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectMembers", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsTranscriptRequested") + .HasColumnType("boolean"); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TranscriptErrorMessage") + .HasColumnType("text"); + + b.Property("TranscriptStatus") + .HasColumnType("integer"); + + b.Property("TranscriptText") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProjectId") + .IsUnique(); + + b.ToTable("Recordings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("Type"); + + b.ToTable("ResourceLinks", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TaskId", "UserId") + .IsUnique(); + + b.ToTable("TaskAssignments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DueDate") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DueDate"); + + b.HasIndex("ProjectId"); + + b.HasIndex("Status"); + + b.ToTable("TaskGens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnableReminders") + .HasColumnType("boolean"); + + b.Property("EscalateReminderPriority") + .HasColumnType("boolean"); + + b.Property("PreferredReminderHour") + .HasColumnType("integer"); + + b.Property("ReminderFrequencyInDays") + .HasColumnType("integer"); + + b.Property("ReminderIntervalInHours") + .HasColumnType("integer"); + + b.Property("StartRemindingDaysBeforeDue") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId") + .IsUnique(); + + b.ToTable("TaskReminderConfigs", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth0Id") + .HasColumnType("text"); + + b.Property("AvatarUrl") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FCMToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Email"); + + b.HasIndex("Platform"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "Platform") + .IsUnique(); + + b.ToTable("UserDeviceTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EndDate"); + + b.HasIndex("IsActive"); + + b.HasIndex("PlanId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPlans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsUnlimited") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PlanFeatureId") + .HasColumnType("uuid"); + + b.Property("QuotaRemaining") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaUsed") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,2)") + .HasDefaultValue(0m); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlanFeatureId"); + + b.HasIndex("UserPlanId", "PlanFeatureId") + .IsUnique(); + + b.ToTable("UserPlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DarkModeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReceiveEmailNotifications") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserSettings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Attachments") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("JwtTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Notifications") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("Notifications") + .HasForeignKey("UserId"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("PaymentTransactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany() + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany("PlanFeatures") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("Features") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("OwnedProjects") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("Members") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("Recordings") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithOne("Recording") + .HasForeignKey("TaskGenix.Core.Domains.Recording", "ProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("ResourceLinks") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Assignments") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "AssignedUser") + .WithMany("TaskAssignments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AssignedUser"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("TaskGens") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithOne("ReminderConfig") + .HasForeignKey("TaskGenix.Core.Domains.TaskReminderConfig", "TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("DeviceTokens") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("UserPlans") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("UserPlans") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plan"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.PlanFeature", "PlanFeature") + .WithMany() + .HasForeignKey("PlanFeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany("Features") + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlanFeature"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithOne("Settings") + .HasForeignKey("TaskGenix.Core.Domains.UserSetting", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Navigation("PlanFeatures"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Navigation("Features"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Navigation("Members"); + + b.Navigation("Recording"); + + b.Navigation("TaskGens"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Navigation("Assignments"); + + b.Navigation("Attachments"); + + b.Navigation("Notifications"); + + b.Navigation("ReminderConfig"); + + b.Navigation("ResourceLinks"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Navigation("DeviceTokens"); + + b.Navigation("JwtTokens"); + + b.Navigation("Notifications"); + + b.Navigation("OwnedProjects"); + + b.Navigation("PaymentTransactions"); + + b.Navigation("ProjectMemberships"); + + b.Navigation("Recordings"); + + b.Navigation("Settings"); + + b.Navigation("TaskAssignments"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Navigation("Features"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251014162747_editForeignKey.cs b/TaskGenix.DAL/Migrations/20251014162747_editForeignKey.cs new file mode 100644 index 0000000..489fcfd --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251014162747_editForeignKey.cs @@ -0,0 +1,84 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + /// + public partial class editForeignKey : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_UserPlanFeatures_Features_FeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures"); + + migrationBuilder.RenameColumn( + name: "FeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + newName: "PlanFeatureId"); + + migrationBuilder.RenameIndex( + name: "IX_UserPlanFeatures_UserPlanId_FeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + newName: "IX_UserPlanFeatures_UserPlanId_PlanFeatureId"); + + migrationBuilder.RenameIndex( + name: "IX_UserPlanFeatures_FeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + newName: "IX_UserPlanFeatures_PlanFeatureId"); + + migrationBuilder.AddForeignKey( + name: "FK_UserPlanFeatures_PlanFeatures_PlanFeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + column: "PlanFeatureId", + principalSchema: "TaskGenixDb", + principalTable: "PlanFeatures", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_UserPlanFeatures_PlanFeatures_PlanFeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures"); + + migrationBuilder.RenameColumn( + name: "PlanFeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + newName: "FeatureId"); + + migrationBuilder.RenameIndex( + name: "IX_UserPlanFeatures_UserPlanId_PlanFeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + newName: "IX_UserPlanFeatures_UserPlanId_FeatureId"); + + migrationBuilder.RenameIndex( + name: "IX_UserPlanFeatures_PlanFeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + newName: "IX_UserPlanFeatures_FeatureId"); + + migrationBuilder.AddForeignKey( + name: "FK_UserPlanFeatures_Features_FeatureId", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + column: "FeatureId", + principalSchema: "TaskGenixDb", + principalTable: "Features", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251014165057_addAttribute.Designer.cs b/TaskGenix.DAL/Migrations/20251014165057_addAttribute.Designer.cs new file mode 100644 index 0000000..51a5596 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251014165057_addAttribute.Designer.cs @@ -0,0 +1,1304 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TaskGenix.DAL.Data; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + [DbContext(typeof(TaskGenixDbContext))] + [Migration("20251014165057_addAttribute")] + partial class addAttribute + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("TaskGenixDb") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FileType"); + + b.HasIndex("TaskGenId"); + + b.ToTable("Attachments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Features", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsExpired") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsMobile") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RefreshToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RefreshTokenExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RefreshToken") + .IsUnique(); + + b.HasIndex("RefreshTokenExpirationDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("JwtTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("BLUE"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStatus") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("PENDING"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Icon") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("MESSAGE"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsSent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ReminderPattern") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("ONCE"); + + b.Property("ReminderSeriesId") + .HasColumnType("uuid"); + + b.Property("ScheduledTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("INFO"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("IsRead"); + + b.HasIndex("IsSent"); + + b.HasIndex("ScheduledTime"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.OtpCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("Email", "Purpose", "IsUsed"); + + b.ToTable("OtpCodes", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentGateway") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransactionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExternalTransactionId"); + + b.HasIndex("Status"); + + b.HasIndex("TransactionDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserPlanId"); + + b.ToTable("PaymentTransactions", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DurationInDays") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Price"); + + b.ToTable("Plans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .HasColumnType("boolean"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("QuotaValue") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("PlanId", "FeatureId") + .IsUnique(); + + b.ToTable("PlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("RecordingId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("Projects", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Role"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectMembers", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("BucketName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsTranscriptRequested") + .HasColumnType("boolean"); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerEmail") + .IsRequired() + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TranscriptErrorMessage") + .HasColumnType("text"); + + b.Property("TranscriptStatus") + .HasColumnType("integer"); + + b.Property("TranscriptText") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProjectId") + .IsUnique(); + + b.ToTable("Recordings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("Type"); + + b.ToTable("ResourceLinks", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TaskId", "UserId") + .IsUnique(); + + b.ToTable("TaskAssignments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DueDate") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DueDate"); + + b.HasIndex("ProjectId"); + + b.HasIndex("Status"); + + b.ToTable("TaskGens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnableReminders") + .HasColumnType("boolean"); + + b.Property("EscalateReminderPriority") + .HasColumnType("boolean"); + + b.Property("PreferredReminderHour") + .HasColumnType("integer"); + + b.Property("ReminderFrequencyInDays") + .HasColumnType("integer"); + + b.Property("ReminderIntervalInHours") + .HasColumnType("integer"); + + b.Property("StartRemindingDaysBeforeDue") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId") + .IsUnique(); + + b.ToTable("TaskReminderConfigs", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth0Id") + .HasColumnType("text"); + + b.Property("AvatarUrl") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FCMToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Email"); + + b.HasIndex("Platform"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "Platform") + .IsUnique(); + + b.ToTable("UserDeviceTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EndDate"); + + b.HasIndex("IsActive"); + + b.HasIndex("PlanId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPlans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsUnlimited") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PlanFeatureId") + .HasColumnType("uuid"); + + b.Property("QuotaRemaining") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaUsed") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,2)") + .HasDefaultValue(0m); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlanFeatureId"); + + b.HasIndex("UserPlanId", "PlanFeatureId") + .IsUnique(); + + b.ToTable("UserPlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DarkModeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReceiveEmailNotifications") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserSettings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Attachments") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("JwtTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Notifications") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("Notifications") + .HasForeignKey("UserId"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("PaymentTransactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany() + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany("PlanFeatures") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("Features") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("OwnedProjects") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("Members") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("Recordings") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithOne("Recording") + .HasForeignKey("TaskGenix.Core.Domains.Recording", "ProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("ResourceLinks") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Assignments") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "AssignedUser") + .WithMany("TaskAssignments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AssignedUser"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("TaskGens") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithOne("ReminderConfig") + .HasForeignKey("TaskGenix.Core.Domains.TaskReminderConfig", "TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("DeviceTokens") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("UserPlans") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("UserPlans") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plan"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.PlanFeature", "PlanFeature") + .WithMany() + .HasForeignKey("PlanFeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany("Features") + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlanFeature"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithOne("Settings") + .HasForeignKey("TaskGenix.Core.Domains.UserSetting", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Navigation("PlanFeatures"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Navigation("Features"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Navigation("Members"); + + b.Navigation("Recording"); + + b.Navigation("TaskGens"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Navigation("Assignments"); + + b.Navigation("Attachments"); + + b.Navigation("Notifications"); + + b.Navigation("ReminderConfig"); + + b.Navigation("ResourceLinks"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Navigation("DeviceTokens"); + + b.Navigation("JwtTokens"); + + b.Navigation("Notifications"); + + b.Navigation("OwnedProjects"); + + b.Navigation("PaymentTransactions"); + + b.Navigation("ProjectMemberships"); + + b.Navigation("Recordings"); + + b.Navigation("Settings"); + + b.Navigation("TaskAssignments"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Navigation("Features"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251014165057_addAttribute.cs b/TaskGenix.DAL/Migrations/20251014165057_addAttribute.cs new file mode 100644 index 0000000..89841b5 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251014165057_addAttribute.cs @@ -0,0 +1,31 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + /// + public partial class addAttribute : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FeatureKey", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FeatureKey", + schema: "TaskGenixDb", + table: "UserPlanFeatures"); + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251017043252_ModifyRecordingEntity.Designer.cs b/TaskGenix.DAL/Migrations/20251017043252_ModifyRecordingEntity.Designer.cs new file mode 100644 index 0000000..14347b9 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251017043252_ModifyRecordingEntity.Designer.cs @@ -0,0 +1,1301 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TaskGenix.DAL.Data; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + [DbContext(typeof(TaskGenixDbContext))] + [Migration("20251017043252_ModifyRecordingEntity")] + partial class ModifyRecordingEntity + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("TaskGenixDb") + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileType") + .IsRequired() + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FileType"); + + b.HasIndex("TaskGenId"); + + b.ToTable("Attachments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Features", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsExpired") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsMobile") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RefreshToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RefreshTokenExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RefreshToken") + .IsUnique(); + + b.HasIndex("RefreshTokenExpirationDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("JwtTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("BLUE"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeliveryStatus") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("PENDING"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Icon") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("MESSAGE"); + + b.Property("IsRead") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsSent") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("ReminderPattern") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("ONCE"); + + b.Property("ReminderSeriesId") + .HasColumnType("uuid"); + + b.Property("ScheduledTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("SequenceNumber") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("INFO"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Email"); + + b.HasIndex("IsRead"); + + b.HasIndex("IsSent"); + + b.HasIndex("ScheduledTime"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.OtpCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("Email", "Purpose", "IsUsed"); + + b.ToTable("OtpCodes", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("character varying(3)"); + + b.Property("ExternalTransactionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PaymentGateway") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TransactionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ExternalTransactionId"); + + b.HasIndex("Status"); + + b.HasIndex("TransactionDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserPlanId"); + + b.ToTable("PaymentTransactions", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("DurationInDays") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Price") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Price"); + + b.ToTable("Plans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureId") + .HasColumnType("uuid"); + + b.Property("IsUnlimited") + .HasColumnType("boolean"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("QuotaValue") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FeatureId"); + + b.HasIndex("PlanId", "FeatureId") + .IsUnique(); + + b.ToTable("PlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("DueDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("RecordingId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name"); + + b.HasIndex("OwnerId"); + + b.ToTable("Projects", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("JoinedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Role"); + + b.HasIndex("UserId"); + + b.HasIndex("ProjectId", "UserId") + .IsUnique(); + + b.ToTable("ProjectMembers", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .HasColumnType("interval"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsTranscriptRequested") + .HasColumnType("boolean"); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ObjectKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("OriginalFileName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("OwnerEmail") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TranscriptErrorMessage") + .HasColumnType("text"); + + b.Property("TranscriptStatus") + .HasColumnType("integer"); + + b.Property("TranscriptText") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.HasIndex("ProjectId") + .IsUnique(); + + b.ToTable("Recordings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId"); + + b.HasIndex("Type"); + + b.ToTable("ResourceLinks", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AssignedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("TaskId", "UserId") + .IsUnique(); + + b.ToTable("TaskAssignments", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("DueDate") + .IsRequired() + .HasColumnType("timestamp with time zone"); + + b.Property("Priority") + .IsRequired() + .HasColumnType("text"); + + b.Property("ProjectId") + .HasColumnType("uuid"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DueDate"); + + b.HasIndex("ProjectId"); + + b.HasIndex("Status"); + + b.ToTable("TaskGens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnableReminders") + .HasColumnType("boolean"); + + b.Property("EscalateReminderPriority") + .HasColumnType("boolean"); + + b.Property("PreferredReminderHour") + .HasColumnType("integer"); + + b.Property("ReminderFrequencyInDays") + .HasColumnType("integer"); + + b.Property("ReminderIntervalInHours") + .HasColumnType("integer"); + + b.Property("StartRemindingDaysBeforeDue") + .HasColumnType("integer"); + + b.Property("TaskGenId") + .HasColumnType("uuid"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("TaskGenId") + .IsUnique(); + + b.ToTable("TaskReminderConfigs", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Auth0Id") + .HasColumnType("text"); + + b.Property("AvatarUrl") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastLogin") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("Role") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Users", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeviceName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("FCMToken") + .IsRequired() + .HasColumnType("text"); + + b.Property("Platform") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId"); + + b.HasIndex("Email"); + + b.HasIndex("Platform"); + + b.HasIndex("UserId"); + + b.HasIndex("DeviceId", "Platform") + .IsUnique(); + + b.ToTable("UserDeviceTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlanId") + .HasColumnType("uuid"); + + b.Property("Price") + .HasColumnType("numeric"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("EndDate"); + + b.HasIndex("IsActive"); + + b.HasIndex("PlanId"); + + b.HasIndex("UserId"); + + b.ToTable("UserPlans", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FeatureKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsUnlimited") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("PlanFeatureId") + .HasColumnType("uuid"); + + b.Property("QuotaRemaining") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("QuotaUsed") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserPlanId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlanFeatureId"); + + b.HasIndex("UserPlanId", "PlanFeatureId") + .IsUnique(); + + b.ToTable("UserPlanFeatures", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DarkModeEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Language") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReceiveEmailNotifications") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("UserSettings", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Attachment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Attachments") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("JwtTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Notifications") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("Notifications") + .HasForeignKey("UserId"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("PaymentTransactions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany() + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("User"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.PlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + .WithMany("PlanFeatures") + .HasForeignKey("FeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("Features") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Feature"); + + b.Navigation("Plan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("OwnedProjects") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ProjectMember", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("Members") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("ProjectMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("Recordings") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithOne("Recording") + .HasForeignKey("TaskGenix.Core.Domains.Recording", "ProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Owner"); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.ResourceLink", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("ResourceLinks") + .HasForeignKey("TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskAssignment", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithMany("Assignments") + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "AssignedUser") + .WithMany("TaskAssignments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AssignedUser"); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.HasOne("TaskGenix.Core.Domains.Project", "Project") + .WithMany("TaskGens") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskReminderConfig", b => + { + b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") + .WithOne("ReminderConfig") + .HasForeignKey("TaskGenix.Core.Domains.TaskReminderConfig", "TaskGenId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TaskGen"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", null) + .WithMany("DeviceTokens") + .HasForeignKey("UserId"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.HasOne("TaskGenix.Core.Domains.Plan", "Plan") + .WithMany("UserPlans") + .HasForeignKey("PlanId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("UserPlans") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Plan"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => + { + b.HasOne("TaskGenix.Core.Domains.PlanFeature", "PlanFeature") + .WithMany() + .HasForeignKey("PlanFeatureId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("TaskGenix.Core.Domains.UserPlan", "UserPlan") + .WithMany("Features") + .HasForeignKey("UserPlanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlanFeature"); + + b.Navigation("UserPlan"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserSetting", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithOne("Settings") + .HasForeignKey("TaskGenix.Core.Domains.UserSetting", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Feature", b => + { + b.Navigation("PlanFeatures"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Plan", b => + { + b.Navigation("Features"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Project", b => + { + b.Navigation("Members"); + + b.Navigation("Recording"); + + b.Navigation("TaskGens"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.TaskGen", b => + { + b.Navigation("Assignments"); + + b.Navigation("Attachments"); + + b.Navigation("Notifications"); + + b.Navigation("ReminderConfig"); + + b.Navigation("ResourceLinks"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.User", b => + { + b.Navigation("DeviceTokens"); + + b.Navigation("JwtTokens"); + + b.Navigation("Notifications"); + + b.Navigation("OwnedProjects"); + + b.Navigation("PaymentTransactions"); + + b.Navigation("ProjectMemberships"); + + b.Navigation("Recordings"); + + b.Navigation("Settings"); + + b.Navigation("TaskAssignments"); + + b.Navigation("UserPlans"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => + { + b.Navigation("Features"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TaskGenix.DAL/Migrations/20251017043252_ModifyRecordingEntity.cs b/TaskGenix.DAL/Migrations/20251017043252_ModifyRecordingEntity.cs new file mode 100644 index 0000000..5ad2a26 --- /dev/null +++ b/TaskGenix.DAL/Migrations/20251017043252_ModifyRecordingEntity.cs @@ -0,0 +1,146 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TaskGenix.DAL.Migrations +{ + /// + public partial class ModifyRecordingEntity : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BucketName", + schema: "TaskGenixDb", + table: "Recordings"); + + migrationBuilder.AlterColumn( + name: "QuotaUsed", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + type: "numeric(18,2)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldDefaultValue: 0m); + + migrationBuilder.AlterColumn( + name: "QuotaTotal", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + type: "numeric(18,2)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)"); + + migrationBuilder.AlterColumn( + name: "QuotaRemaining", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + type: "numeric(18,2)", + nullable: true, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)"); + + migrationBuilder.AlterColumn( + name: "OwnerEmail", + schema: "TaskGenixDb", + table: "Recordings", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AlterColumn( + name: "Duration", + schema: "TaskGenixDb", + table: "Recordings", + type: "interval", + nullable: true, + oldClrType: typeof(TimeSpan), + oldType: "interval"); + + migrationBuilder.AddColumn( + name: "OriginalFileName", + schema: "TaskGenixDb", + table: "Recordings", + type: "character varying(255)", + maxLength: 255, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "OriginalFileName", + schema: "TaskGenixDb", + table: "Recordings"); + + migrationBuilder.AlterColumn( + name: "QuotaUsed", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + type: "numeric(18,2)", + nullable: false, + defaultValue: 0m, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "QuotaTotal", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + type: "numeric(18,2)", + nullable: false, + defaultValue: 0m, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "QuotaRemaining", + schema: "TaskGenixDb", + table: "UserPlanFeatures", + type: "numeric(18,2)", + nullable: false, + defaultValue: 0m, + oldClrType: typeof(decimal), + oldType: "numeric(18,2)", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "OwnerEmail", + schema: "TaskGenixDb", + table: "Recordings", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Duration", + schema: "TaskGenixDb", + table: "Recordings", + type: "interval", + nullable: false, + defaultValue: new TimeSpan(0, 0, 0, 0, 0), + oldClrType: typeof(TimeSpan), + oldType: "interval", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "BucketName", + schema: "TaskGenixDb", + table: "Recordings", + type: "text", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/TaskGenix.DAL/Migrations/TaskGenixDbContextModelSnapshot.cs b/TaskGenix.DAL/Migrations/TaskGenixDbContextModelSnapshot.cs index 065fc1f..d4d95c6 100644 --- a/TaskGenix.DAL/Migrations/TaskGenixDbContextModelSnapshot.cs +++ b/TaskGenix.DAL/Migrations/TaskGenixDbContextModelSnapshot.cs @@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) #pragma warning disable 612, 618 modelBuilder .HasDefaultSchema("TaskGenixDb") - .HasAnnotation("ProductVersion", "8.0.19") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -30,7 +30,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Data") .IsRequired() @@ -52,7 +52,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -70,7 +70,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("FeatureType") .IsRequired() @@ -92,7 +92,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(50)"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -102,22 +102,102 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Features", "TaskGenixDb"); }); - modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("uuid"); + b.Property("AccessToken") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceInfo") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); - b.Property("DeliveryChannel") + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsExpired") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsMobile") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("IsRevoked") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("RefreshToken") .IsRequired() - .HasColumnType("text"); + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RefreshTokenExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("RefreshToken") + .IsUnique(); + + b.HasIndex("RefreshTokenExpirationDate"); + + b.HasIndex("UserId"); + + b.HasIndex("UserId", "IsRevoked"); + + b.ToTable("JwtTokens", "TaskGenixDb"); + }); + + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Color") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("BLUE"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); b.Property("DeliveryStatus") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("PENDING"); + + b.Property("Email") + .IsRequired() .HasColumnType("text"); + b.Property("Icon") + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("MESSAGE"); + b.Property("IsRead") .ValueGeneratedOnAdd() .HasColumnType("boolean") @@ -134,7 +214,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(1000)"); b.Property("ReminderPattern") - .HasColumnType("text"); + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasDefaultValue("ONCE"); b.Property("ReminderSeriesId") .HasColumnType("uuid"); @@ -160,16 +242,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .ValueGeneratedOnAdd() .HasColumnType("text") - .HasDefaultValue("Information"); + .HasDefaultValue("INFO"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UserId") .HasColumnType("uuid"); b.HasKey("Id"); + b.HasIndex("Email"); + b.HasIndex("IsRead"); b.HasIndex("IsSent"); @@ -183,6 +267,60 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Notifications", "TaskGenixDb"); }); + modelBuilder.Entity("TaskGenix.Core.Domains.OtpCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AttemptCount") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("IsUsed") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false); + + b.Property("Purpose") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpirationDate"); + + b.HasIndex("Email", "Purpose", "IsUsed"); + + b.ToTable("OtpCodes", "TaskGenixDb"); + }); + modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => { b.Property("Id") @@ -193,7 +331,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("decimal(18,2)"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Currency") .IsRequired() @@ -218,10 +356,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("TransactionDate") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); @@ -251,7 +389,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Description") .IsRequired() @@ -270,7 +408,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("decimal(18,2)"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -286,7 +424,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("FeatureId") .HasColumnType("uuid"); @@ -301,7 +439,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("numeric"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -320,7 +458,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Description") .IsRequired() @@ -330,6 +468,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DueDate") .HasColumnType("timestamp with time zone"); + b.Property("IsActive") + .HasColumnType("boolean"); + b.Property("Name") .IsRequired() .HasMaxLength(200) @@ -350,10 +491,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("IsActive"); + b.HasIndex("Name"); b.HasIndex("OwnerId"); @@ -368,10 +511,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("JoinedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("ProjectId") .HasColumnType("uuid"); @@ -381,7 +524,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); @@ -407,14 +550,34 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("CreatedAt") .HasColumnType("timestamp with time zone"); - b.Property("Duration") + b.Property("Duration") .HasColumnType("interval"); - b.Property("FileUrl") + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("IsTranscriptRequested") + .HasColumnType("boolean"); + + b.Property("MimeType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ObjectKey") .IsRequired() .HasColumnType("text"); - b.Property("ProjectId") + b.Property("OriginalFileName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("OwnerEmail") + .HasColumnType("text"); + + b.Property("OwnerId") + .HasColumnType("uuid"); + + b.Property("ProjectId") .HasColumnType("uuid"); b.Property("Title") @@ -422,19 +585,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("character varying(200)"); + b.Property("TranscriptErrorMessage") + .HasColumnType("text"); + + b.Property("TranscriptStatus") + .HasColumnType("integer"); + + b.Property("TranscriptText") + .HasColumnType("text"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") - .HasColumnType("uuid"); + b.Property("UploadedAt") + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); + b.HasIndex("OwnerId"); + b.HasIndex("ProjectId") .IsUnique(); - b.HasIndex("UserId"); - b.ToTable("Recordings", "TaskGenixDb"); }); @@ -445,7 +617,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Description") .HasColumnType("text"); @@ -463,7 +635,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Url") .IsRequired() @@ -485,7 +657,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("AssignedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("TaskId") .HasColumnType("uuid"); @@ -510,7 +682,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Description") .IsRequired() @@ -538,7 +710,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("character varying(200)"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -558,7 +730,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("EnableReminders") .HasColumnType("boolean"); @@ -566,10 +738,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EscalateReminderPriority") .HasColumnType("boolean"); - b.Property("PreferredChannel") - .IsRequired() - .HasColumnType("text"); - b.Property("PreferredReminderHour") .HasColumnType("integer"); @@ -586,7 +754,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.HasKey("Id"); @@ -602,11 +770,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("uuid"); - b.Property("AvatarUrl") + b.Property("Auth0Id") .HasColumnType("text"); + b.Property("AvatarUrl") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Email") .IsRequired() @@ -631,7 +803,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("Username") .IsRequired() @@ -666,6 +838,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + b.Property("FCMToken") .IsRequired() .HasColumnType("text"); @@ -677,13 +853,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("UpdatedAt") .HasColumnType("timestamp with time zone"); - b.Property("UserId") + b.Property("UserId") .HasColumnType("uuid"); b.HasKey("Id"); b.HasIndex("DeviceId"); + b.HasIndex("Email"); + b.HasIndex("Platform"); b.HasIndex("UserId"); @@ -701,7 +879,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("EndDate") .HasColumnType("timestamp with time zone"); @@ -723,7 +901,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("timestamp with time zone"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); @@ -748,38 +926,40 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); - b.Property("FeatureId") - .HasColumnType("uuid"); + b.Property("FeatureKey") + .IsRequired() + .HasColumnType("text"); b.Property("IsUnlimited") .ValueGeneratedOnAdd() .HasColumnType("boolean") .HasDefaultValue(false); - b.Property("QuotaRemaining") + b.Property("PlanFeatureId") + .HasColumnType("uuid"); + + b.Property("QuotaRemaining") .HasColumnType("decimal(18,2)"); - b.Property("QuotaTotal") + b.Property("QuotaTotal") .HasColumnType("decimal(18,2)"); - b.Property("QuotaUsed") - .ValueGeneratedOnAdd() - .HasColumnType("decimal(18,2)") - .HasDefaultValue(0m); + b.Property("QuotaUsed") + .HasColumnType("decimal(18,2)"); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserPlanId") .HasColumnType("uuid"); b.HasKey("Id"); - b.HasIndex("FeatureId"); + b.HasIndex("PlanFeatureId"); - b.HasIndex("UserPlanId", "FeatureId") + b.HasIndex("UserPlanId", "PlanFeatureId") .IsUnique(); b.ToTable("UserPlanFeatures", "TaskGenixDb"); @@ -792,7 +972,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uuid"); b.Property("CreatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("DarkModeEnabled") .ValueGeneratedOnAdd() @@ -809,7 +989,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasDefaultValue(true); b.Property("UpdatedAt") - .HasColumnType("timestamp without time zone"); + .HasColumnType("timestamp with time zone"); b.Property("UserId") .HasColumnType("uuid"); @@ -833,6 +1013,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("TaskGen"); }); + modelBuilder.Entity("TaskGenix.Core.Domains.JwtToken", b => + { + b.HasOne("TaskGenix.Core.Domains.User", "User") + .WithMany("JwtTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("TaskGenix.Core.Domains.Notification", b => { b.HasOne("TaskGenix.Core.Domains.TaskGen", "TaskGen") @@ -840,15 +1031,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("TaskGenId") .OnDelete(DeleteBehavior.SetNull); - b.HasOne("TaskGenix.Core.Domains.User", "User") + b.HasOne("TaskGenix.Core.Domains.User", null) .WithMany("Notifications") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("UserId"); b.Navigation("TaskGen"); - - b.Navigation("User"); }); modelBuilder.Entity("TaskGenix.Core.Domains.PaymentTransaction", b => @@ -921,17 +1108,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("TaskGenix.Core.Domains.Recording", b => { + b.HasOne("TaskGenix.Core.Domains.User", "Owner") + .WithMany("Recordings") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.SetNull); + b.HasOne("TaskGenix.Core.Domains.Project", "Project") .WithOne("Recording") .HasForeignKey("TaskGenix.Core.Domains.Recording", "ProjectId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("TaskGenix.Core.Domains.User", "Owner") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); + .OnDelete(DeleteBehavior.Cascade); b.Navigation("Owner"); @@ -992,13 +1177,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("TaskGenix.Core.Domains.UserDeviceToken", b => { - b.HasOne("TaskGenix.Core.Domains.User", "User") + b.HasOne("TaskGenix.Core.Domains.User", null) .WithMany("DeviceTokens") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("User"); + .HasForeignKey("UserId"); }); modelBuilder.Entity("TaskGenix.Core.Domains.UserPlan", b => @@ -1022,9 +1203,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("TaskGenix.Core.Domains.UserPlanFeature", b => { - b.HasOne("TaskGenix.Core.Domains.Feature", "Feature") + b.HasOne("TaskGenix.Core.Domains.PlanFeature", "PlanFeature") .WithMany() - .HasForeignKey("FeatureId") + .HasForeignKey("PlanFeatureId") .OnDelete(DeleteBehavior.Restrict) .IsRequired(); @@ -1034,7 +1215,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .OnDelete(DeleteBehavior.Cascade) .IsRequired(); - b.Navigation("Feature"); + b.Navigation("PlanFeature"); b.Navigation("UserPlan"); }); @@ -1088,6 +1269,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Navigation("DeviceTokens"); + b.Navigation("JwtTokens"); + b.Navigation("Notifications"); b.Navigation("OwnedProjects"); @@ -1096,6 +1279,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ProjectMemberships"); + b.Navigation("Recordings"); + b.Navigation("Settings"); b.Navigation("TaskAssignments"); diff --git a/TaskGenix.DAL/Repositories/Implements/GenericRepository.cs b/TaskGenix.DAL/Repositories/Implements/GenericRepository.cs index a6c41f7..5f3b824 100644 --- a/TaskGenix.DAL/Repositories/Implements/GenericRepository.cs +++ b/TaskGenix.DAL/Repositories/Implements/GenericRepository.cs @@ -6,310 +6,302 @@ using TaskGenix.DAL.Repositories.Interfaces; using TaskGenix.Repositories.Interfaces; -namespace TaskGenix.DAL.Repositories.Implements +namespace TaskGenix.DAL.Repositories.Implements; + +public class GenericRepository : IGenericRepository where T : class { - public class GenericRepository : IGenericRepository where T : class + protected readonly DbContext _dbContext; + protected readonly DbSet _dbSet; + + public GenericRepository(DbContext context) { - protected readonly DbContext _dbContext; - protected readonly DbSet _dbSet; + _dbContext = context; + _dbSet = context.Set(); + } - public GenericRepository(DbContext context) - { - _dbContext = context; - _dbSet = context.Set(); - } + public void Dispose() + { + _dbContext?.Dispose(); + } - public void Dispose() - { - _dbContext?.Dispose(); - } + #region Get Async - #region Get Async + public virtual async Task FirstOrDefaultAsync( + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null) + { + IQueryable query = _dbSet; + if (include != null) query = include(query); - public virtual async Task FirstOrDefaultAsync( - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null) - { - IQueryable query = _dbSet; - if (include != null) query = include(query); + if (predicate != null) query = query.Where(predicate); - if (predicate != null) query = query.Where(predicate); + if (orderBy != null) query = orderBy(query); - if (orderBy != null) query = orderBy(query); + return await query.FirstOrDefaultAsync(); + } - return await query.FirstOrDefaultAsync(); - } + public virtual async Task FirstOrDefaultAsync( + Expression> selector, + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null) + { + IQueryable query = _dbSet; + if (include != null) query = include(query); - public virtual async Task FirstOrDefaultAsync( - Expression> selector, - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null) - { - IQueryable query = _dbSet; - if (include != null) query = include(query); + if (predicate != null) query = query.Where(predicate); - if (predicate != null) query = query.Where(predicate); + if (orderBy != null) query = orderBy(query); - if (orderBy != null) query = orderBy(query); + return await query.Select(selector).FirstOrDefaultAsync(); + } - return await query.Select(selector).FirstOrDefaultAsync(); - } + public virtual async Task SingleOrDefaultAsync( + Expression>? predicate = null, + Func, IQueryable>? include = null) + { + IQueryable query = _dbSet; - public virtual async Task SingleOrDefaultAsync( - Expression> predicate = null, - Func, IQueryable> include = null) - { - IQueryable query = _dbSet; + if (include != null) + query = include(query); - if (include != null) - query = include(query); + if (predicate != null) + query = query.Where(predicate); - if (predicate != null) - query = query.Where(predicate); + return await query.SingleOrDefaultAsync(); + } - return await query.SingleOrDefaultAsync(); - } + public virtual async Task SingleOrDefaultAsync( + Expression> selector, + Expression>? predicate = null, + Func, IQueryable>? include = null) + { + IQueryable query = _dbSet; + if (include != null) query = include(query); - public virtual async Task SingleOrDefaultAsync( - Expression> selector, - Expression> predicate = null, - Func, IQueryable> include = null) - { - IQueryable query = _dbSet; - if (include != null) query = include(query); + if (predicate != null) query = query.Where(predicate); - if (predicate != null) query = query.Where(predicate); + return await query.Select(selector).SingleOrDefaultAsync(); + } - return await query.Select(selector).SingleOrDefaultAsync(); - } + public virtual async Task> GetListAsync( + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null, + int? take = null) + { + IQueryable query = _dbSet; - public virtual async Task> GetListAsync( - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null, - int? take = null) - { - IQueryable query = _dbSet; + if (include != null) query = include(query); - if (include != null) query = include(query); + if (predicate != null) query = query.Where(predicate); - if (predicate != null) query = query.Where(predicate); + if (orderBy != null) query = orderBy(query); - if (orderBy != null) query = orderBy(query); + if (take.HasValue) query = query.Take(take.Value); - if (take.HasValue) query = query.Take(take.Value); + return await query.ToListAsync(); + } - return await query.ToListAsync(); - } + public virtual async Task> GetListAsync( + Expression> selector, + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null, + int? take = null) + { + IQueryable query = _dbSet; - public virtual async Task> GetListAsync( - Expression> selector, - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null, - int? take = null) - { - IQueryable query = _dbSet; + if (include != null) query = include(query); - if (include != null) query = include(query); + if (predicate != null) query = query.Where(predicate); - if (predicate != null) query = query.Where(predicate); + if (orderBy != null) query = orderBy(query); - if (orderBy != null) query = orderBy(query); + var resultQuery = query.Select(selector); - IQueryable resultQuery = query.Select(selector); + if (take.HasValue) resultQuery = resultQuery.Take(take.Value); - if (take.HasValue) resultQuery = resultQuery.Take(take.Value); + return await resultQuery.ToListAsync(); + } - return await resultQuery.ToListAsync(); - } + public Task> GetPagingListAsync( + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null, + int page = 1, + int size = 10) + { + IQueryable query = _dbSet; + if (include != null) query = include(query); + if (predicate != null) query = query.Where(predicate); + if (orderBy != null) return orderBy(query).ToPaginatedResponse(page, size, 1); + return query.ToPaginatedResponse(page, size, 1); + } + + public Task> GetPagingListAsync( + Expression> selector, + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null, + int page = 1, + int size = 10) + { + IQueryable query = _dbSet; + if (include != null) query = include(query); + if (predicate != null) query = query.Where(predicate); + if (orderBy != null) return orderBy(query).Select(selector).ToPaginatedResponse(page, size, 1); + return query.Select(selector).ToPaginatedResponse(page, size, 1); + } + + public virtual async Task CountAsync(Expression>? predicate = null) + { + if (predicate != null) return await _dbSet.CountAsync(predicate); + return await _dbSet.CountAsync(); + } + + #endregion + + #region Insert - public Task> GetPagingListAsync( - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null, - int page = 1, - int size = 10) + public async Task Insert(T entity) + { + if (entity == null) return false; + await _dbSet.AddAsync(entity); + return true; + } + + public async Task InsertRange(IEnumerable entities) + { + if (entities == null || !entities.Any()) return false; + await _dbSet.AddRangeAsync(entities); + return true; + } + + #endregion + + #region Update + + public virtual async Task Update(T entity) + { + if (entity == null) return false; + + try { - IQueryable query = _dbSet; - if (include != null) query = include(query); - if (predicate != null) query = query.Where(predicate); - if (orderBy != null) return orderBy(query).ToPaginatedResponse(page, size, 1); - return query.ToPaginatedResponse(page, size, 1); - } + // Get the entry for the entity + var entityEntry = _dbContext.Entry(entity); - public Task> GetPagingListAsync( - Expression> selector, - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null, - int page = 1, - int size = 10) + // If entity is detached (not being tracked), attach it + if (entityEntry.State == EntityState.Detached) _dbSet.Attach(entity); + + // Mark entity as modified + entityEntry.State = EntityState.Modified; + + await Task.CompletedTask; + return true; + } + catch (Exception) { - IQueryable query = _dbSet; - if (include != null) query = include(query); - if (predicate != null) query = query.Where(predicate); - if (orderBy != null) return orderBy(query).Select(selector).ToPaginatedResponse(page, size, 1); - return query.Select(selector).ToPaginatedResponse(page, size, 1); + await Task.CompletedTask; + return false; } + } - public virtual async Task CountAsync(Expression> predicate = null) + // Add a new method for selective property updates + public virtual async Task UpdateProperties(T entity, params Expression>[] propertiesToUpdate) + { + if (entity == null) return false; + + try { - if (predicate != null) return await _dbSet.CountAsync(predicate); - return await _dbSet.CountAsync(); - } + if (propertiesToUpdate == null || propertiesToUpdate.Length == 0) return await Update(entity); - #endregion + var entityEntry = _dbContext.Entry(entity); - #region Insert + // If entity is detached (not being tracked), attach it + if (entityEntry.State == EntityState.Detached) _dbSet.Attach(entity); + + // Mark only specific properties as modified + foreach (var property in propertiesToUpdate) entityEntry.Property(property).IsModified = true; - public async Task Insert(T entity) - { - if (entity == null) return false; - await _dbSet.AddAsync(entity); return true; } - - public async Task InsertRange(IEnumerable entities) + catch (Exception) { - if (entities == null || !entities.Any()) return false; - await _dbSet.AddRangeAsync(entities); - return true; + return false; } + } - #endregion + public virtual async Task UpdateRange(IEnumerable entities) + { + if (entities == null || !entities.Any()) return false; - #region Update - public virtual async Task Update(T entity) + try { - if (entity == null) return false; - - try - { - // Get the entry for the entity - var entityEntry = _dbContext.Entry(entity); - - // If entity is detached (not being tracked), attach it - if (entityEntry.State == EntityState.Detached) - { - _dbSet.Attach(entity); - } - - // Mark entity as modified - entityEntry.State = EntityState.Modified; - - await Task.CompletedTask; - return true; - } - catch (Exception) - { - await Task.CompletedTask; - return false; - } + // Use EF Core's built-in UpdateRange method for better performance + _dbSet.UpdateRange(entities); + await Task.CompletedTask; + return true; } - - // Add a new method for selective property updates - public virtual async Task UpdateProperties(T entity, params Expression>[] propertiesToUpdate) + catch (Exception) { - if (entity == null) return false; - - try - { - if (propertiesToUpdate == null || propertiesToUpdate.Length == 0) - { - return await Update(entity); - } - - var entityEntry = _dbContext.Entry(entity); - - // If entity is detached (not being tracked), attach it - if (entityEntry.State == EntityState.Detached) - { - _dbSet.Attach(entity); - } - - // Mark only specific properties as modified - foreach (var property in propertiesToUpdate) - { - entityEntry.Property(property).IsModified = true; - } - - return true; - } - catch (Exception) - { - return false; - } + await Task.CompletedTask; + return false; } + } - public virtual async Task UpdateRange(IEnumerable entities) + #endregion + + #region delete + + public virtual async Task Delete(T entity) + { + if (entity == null) return false; + + try { - if (entities == null || !entities.Any()) return false; - - try - { - // Use EF Core's built-in UpdateRange method for better performance - _dbSet.UpdateRange(entities); - await Task.CompletedTask; - return true; - } - catch (Exception) - { - await Task.CompletedTask; - return false; - } + _dbSet.Remove(entity); + await Task.CompletedTask; + return true; } - #endregion - - #region delete - public virtual async Task Delete(T entity) + catch (Exception) { - if (entity == null) return false; - - try - { - _dbSet.Remove(entity); - await Task.CompletedTask; - return true; - } - catch (Exception) - { - return false; - } + return false; } + } - public virtual async Task DeleteRange(IEnumerable entities) + public virtual async Task DeleteRange(IEnumerable entities) + { + if (entities == null || !entities.Any()) return false; + + try { - if (entities == null || !entities.Any()) return false; - - try - { - _dbSet.RemoveRange(entities); - await Task.CompletedTask; - return true; - } - catch (Exception) - { - return false; - } + _dbSet.RemoveRange(entities); + await Task.CompletedTask; + return true; } - #endregion - - #region queryable - /// - /// Tạo query base từ DbSet, cho phép chọn có AsNoTracking hay không. - /// Caller tự LINQ (Where, Include, OrderBy...) để viết query. - /// - public virtual IQueryable CreateBaseQuery(bool asNoTracking = true) + catch (Exception) { - return asNoTracking - ? _dbSet.AsNoTracking() - : _dbSet; + return false; } - #endregion + } + + #endregion + + #region queryable + /// + /// Tạo query base từ DbSet, cho phép chọn có AsNoTracking hay không. + /// Caller tự LINQ (Where, Include, OrderBy...) để viết query. + /// + public virtual IQueryable CreateBaseQuery(bool asNoTracking = true) + { + return asNoTracking + ? _dbSet.AsNoTracking() + : _dbSet; } -} + + #endregion +} \ No newline at end of file diff --git a/TaskGenix.DAL/Repositories/Implements/RecordingRepository.cs b/TaskGenix.DAL/Repositories/Implements/RecordingRepository.cs new file mode 100644 index 0000000..556e4bd --- /dev/null +++ b/TaskGenix.DAL/Repositories/Implements/RecordingRepository.cs @@ -0,0 +1,17 @@ +using TaskGenix.Core.Domains; +using TaskGenix.DAL.Data; +using TaskGenix.DAL.Repositories.Interfaces; + +namespace TaskGenix.DAL.Repositories.Implements; + +public class RecordingRepository : GenericRepository, IRecordingRepository +{ + private readonly TaskGenixDbContext _context; + + public RecordingRepository(TaskGenixDbContext context) : base(context) + { + _context = context; + } + + // Implement specific methods if needed +} \ No newline at end of file diff --git a/TaskGenix.DAL/Repositories/Implements/TaskAssignmentRepository.cs b/TaskGenix.DAL/Repositories/Implements/TaskAssignmentRepository.cs new file mode 100644 index 0000000..1cd08a4 --- /dev/null +++ b/TaskGenix.DAL/Repositories/Implements/TaskAssignmentRepository.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore; +using TaskGenix.Core.Domains; +using TaskGenix.DAL.Data; +using TaskGenix.DAL.Repositories.Interfaces; + +namespace TaskGenix.DAL.Repositories.Implements; + +public class TaskAssignmentRepository : GenericRepository, ITaskAssignmentRepository +{ + private readonly TaskGenixDbContext _context; + + public TaskAssignmentRepository(TaskGenixDbContext context) : base(context) + { + _context = context; + } + + public async Task> GetAssignmentsByTaskIdAsync(Guid taskId) + { + return await _dbSet + .Include(ta => ta.AssignedUser) + .Include(ta => ta.TaskGen) + .Where(ta => ta.TaskId == taskId) + .OrderByDescending(ta => ta.AssignedAt) + .ToListAsync(); + } + + public async Task> GetAssignmentsByUserIdAsync(Guid userId) + { + return await _dbSet + .Include(ta => ta.AssignedUser) + .Include(ta => ta.TaskGen) + .ThenInclude(t => t.Project) + .Where(ta => ta.UserId == userId) + .OrderByDescending(ta => ta.AssignedAt) + .ToListAsync(); + } + + public async Task IsUserAssignedToTaskAsync(Guid taskId, Guid userId) + { + return await _dbSet + .AnyAsync(ta => ta.TaskId == taskId && ta.UserId == userId); + } + + public async Task GetAssignmentByTaskAndUserAsync(Guid taskId, Guid userId) + { + return await _dbSet + .Include(ta => ta.AssignedUser) + .Include(ta => ta.TaskGen) + .FirstOrDefaultAsync(ta => ta.TaskId == taskId && ta.UserId == userId); + } + + public async Task RemoveAllAssignmentsForTaskAsync(Guid taskId) + { + var assignments = await _dbSet + .Where(ta => ta.TaskId == taskId) + .ToListAsync(); + + if (!assignments.Any()) + return true; + + _dbSet.RemoveRange(assignments); + return await _context.SaveChangesAsync() > 0; + } + + public async Task GetAssignmentWithDetailsAsync(Guid assignmentId) + { + return await _dbSet + .Include(ta => ta.AssignedUser) + .Include(ta => ta.TaskGen) + .ThenInclude(t => t.Project) + .FirstOrDefaultAsync(ta => ta.Id == assignmentId); + } +} diff --git a/TaskGenix.DAL/Repositories/Implements/UnitOfWork.cs b/TaskGenix.DAL/Repositories/Implements/UnitOfWork.cs index e8c2b0b..692dc13 100644 --- a/TaskGenix.DAL/Repositories/Implements/UnitOfWork.cs +++ b/TaskGenix.DAL/Repositories/Implements/UnitOfWork.cs @@ -3,110 +3,131 @@ using System.ComponentModel.DataAnnotations; using TaskGenix.DAL.Repositories.Interfaces; using TaskGenix.DAL.Repositories.Implements; +using TaskGenix.Core.Domains; -namespace TaskGenix.Repositories.Implements +namespace TaskGenix.Repositories.Implements; + +public class UnitOfWork : IUnitOfWork where TContext : DbContext { - public class UnitOfWork : IUnitOfWork where TContext : DbContext + public TContext Context { get; } + private Dictionary _repositories; + + public UnitOfWork(TContext context) { - public TContext Context { get; } - private Dictionary _repositories; + Context = context; + } - public UnitOfWork(TContext context) - { - Context = context; - } + #region Repository Management - #region Repository Management - public IGenericRepository GetRepository() where TEntity : class - { - _repositories ??= new Dictionary(); - if (_repositories.TryGetValue(typeof(TEntity), out object repository)) - { - return (IGenericRepository)repository; - } + public IGenericRepository GetRepository() where TEntity : class + { + _repositories ??= new Dictionary(); + if (_repositories.TryGetValue(typeof(TEntity), out var repository)) + return (IGenericRepository)repository; - repository = new GenericRepository(Context); + // Return specialized repository for TaskAssignment + if (typeof(TEntity) == typeof(TaskAssignment) && Context is TaskGenix.DAL.Data.TaskGenixDbContext dbContext) + { + repository = new TaskAssignmentRepository(dbContext); _repositories.Add(typeof(TEntity), repository); return (IGenericRepository)repository; } - #endregion - #region Transaction Management - public async Task ExecuteInTransactionAsync(Func> operation) + // Return specialized repository for Recording + if (typeof(TEntity) == typeof(Recording) && Context is TaskGenix.DAL.Data.TaskGenixDbContext dbContext2) { - var executionStrategy = Context.Database.CreateExecutionStrategy(); - return await executionStrategy.ExecuteAsync(async () => - { - await using var transaction = await Context.Database.BeginTransactionAsync(); - try - { - var result = await operation(); - await Context.SaveChangesAsync(); - await transaction.CommitAsync(); - return result; - } - catch - { - await transaction.RollbackAsync(); - throw; - } - }); + repository = new RecordingRepository(dbContext2); + _repositories.Add(typeof(TEntity), repository); + return (IGenericRepository)repository; } - public async Task ExecuteInTransactionAsync(Func operation) - { - var executionStrategy = Context.Database.CreateExecutionStrategy(); - await executionStrategy.ExecuteAsync(async () => - { - await using var transaction = await Context.Database.BeginTransactionAsync(); - try - { - await operation(); - await Context.SaveChangesAsync(); - await transaction.CommitAsync(); - } - catch - { - await transaction.RollbackAsync(); - throw; - } - }); - } + repository = new GenericRepository(Context); + _repositories.Add(typeof(TEntity), repository); + return (IGenericRepository)repository; + } - public int Commit() - { - TrackChanges(); - return Context.SaveChanges(); - } + #endregion + + #region Transaction Management - public async Task CommitAsync() + public async Task ExecuteInTransactionAsync(Func> operation) + { + var executionStrategy = Context.Database.CreateExecutionStrategy(); + return await executionStrategy.ExecuteAsync(async () => { - TrackChanges(); - return await Context.SaveChangesAsync(); - } + await using var transaction = await Context.Database.BeginTransactionAsync(); + try + { + var result = await operation(); + await Context.SaveChangesAsync(); + await transaction.CommitAsync(); + return result; + } + catch + { + await transaction.RollbackAsync(); + throw; + } + }); + } - private void TrackChanges() + public async Task ExecuteInTransactionAsync(Func operation) + { + var executionStrategy = Context.Database.CreateExecutionStrategy(); + await executionStrategy.ExecuteAsync(async () => { - var validationErrors = Context.ChangeTracker.Entries() - .SelectMany(e => e.Entity.Validate(null)) - .Where(e => e != ValidationResult.Success) - .ToArray(); - if (validationErrors.Any()) + await using var transaction = await Context.Database.BeginTransactionAsync(); + try { - var exceptionMessage = string.Join(Environment.NewLine, - validationErrors.Select(error => $"Properties {string.Join(", ", error.MemberNames)} Error: {error.ErrorMessage}")); - throw new ValidationException(exceptionMessage); + await operation(); + await Context.SaveChangesAsync(); + TrackChanges(); + await transaction.CommitAsync(); } - } + catch + { + await transaction.RollbackAsync(); + throw; + } + }); + } + + public int Commit() + { + TrackChanges(); + return Context.SaveChanges(); + } - #endregion + public async Task CommitAsync() + { + TrackChanges(); + return await Context.SaveChangesAsync(); + } - #region IDisposable Implementation - public void Dispose() + private void TrackChanges() + { + var validationErrors = Context.ChangeTracker.Entries() + .SelectMany(e => e.Entity.Validate(null)) + .Where(e => e != ValidationResult.Success) + .ToArray(); + if (validationErrors.Any()) { - Context?.Dispose(); - GC.SuppressFinalize(this); + var exceptionMessage = string.Join(Environment.NewLine, + validationErrors.Select(error => + $"Properties {string.Join(", ", error.MemberNames)} Error: {error.ErrorMessage}")); + throw new ValidationException(exceptionMessage); } - #endregion } -} + + #endregion + + #region IDisposable Implementation + + public void Dispose() + { + Context?.Dispose(); + GC.SuppressFinalize(this); + } + + #endregion +} \ No newline at end of file diff --git a/TaskGenix.DAL/Repositories/Interfaces/IGenericRepository.cs b/TaskGenix.DAL/Repositories/Interfaces/IGenericRepository.cs index b7bcb2c..46b6260 100644 --- a/TaskGenix.DAL/Repositories/Interfaces/IGenericRepository.cs +++ b/TaskGenix.DAL/Repositories/Interfaces/IGenericRepository.cs @@ -2,86 +2,89 @@ using System.Linq.Expressions; using TaskGenix.Core.Meta; -namespace TaskGenix.DAL.Repositories.Interfaces +namespace TaskGenix.DAL.Repositories.Interfaces; + +public interface IGenericRepository : IDisposable where T : class { - public interface IGenericRepository : IDisposable where T : class - { - #region Get Async - - Task FirstOrDefaultAsync( - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null); - - Task FirstOrDefaultAsync( - Expression> selector, - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null); - - Task SingleOrDefaultAsync( - Expression> predicate = null, - Func, IQueryable> include = null); - - Task SingleOrDefaultAsync( - Expression> selector, - Expression> predicate = null, - Func, IQueryable> include = null); - - Task> GetListAsync( - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null, - int? take = null); - - Task> GetListAsync( - Expression> selector, - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null, - int? take = null); - - Task> GetPagingListAsync( - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null, - int page = 1, - int size = 10); - - Task> GetPagingListAsync( - Expression> selector, - Expression> predicate = null, - Func, IOrderedQueryable> orderBy = null, - Func, IQueryable> include = null, - int page = 1, - int size = 10); - - Task CountAsync(Expression> predicate = null); - - #endregion - - #region Insert - - Task Insert(T entity); - Task InsertRange(IEnumerable entities); - - #endregion - - #region Update - - Task Update(T entity); - - Task UpdateRange(IEnumerable entities); - - #endregion - - #region Delete - Task Delete(T entity); - Task DeleteRange(IEnumerable entities); - #endregion - - #region queryable - IQueryable CreateBaseQuery(bool asNoTracking = true); - #endregion - } -} + #region Get Async + + Task FirstOrDefaultAsync( + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null); + + Task FirstOrDefaultAsync( + Expression> selector, + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null); + + Task SingleOrDefaultAsync( + Expression>? predicate = null, + Func, IQueryable>? include = null); + + Task SingleOrDefaultAsync( + Expression> selector, + Expression>? predicate = null, + Func, IQueryable>? include = null); + + Task> GetListAsync( + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null, + int? take = null); + + Task> GetListAsync( + Expression> selector, + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null, + int? take = null); + + Task> GetPagingListAsync( + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null, + int page = 1, + int size = 10); + + Task> GetPagingListAsync( + Expression> selector, + Expression>? predicate = null, + Func, IOrderedQueryable>? orderBy = null, + Func, IQueryable>? include = null, + int page = 1, + int size = 10); + + Task CountAsync(Expression>? predicate = null); + + #endregion + + #region Insert + + Task Insert(T entity); + Task InsertRange(IEnumerable entities); + + #endregion + + #region Update + + Task Update(T entity); + + Task UpdateRange(IEnumerable entities); + + #endregion + + #region Delete + + Task Delete(T entity); + Task DeleteRange(IEnumerable entities); + + #endregion + + #region queryable + + IQueryable CreateBaseQuery(bool asNoTracking = true); + + #endregion +} \ No newline at end of file diff --git a/TaskGenix.DAL/Repositories/Interfaces/IGenericRepositoryFactory.cs b/TaskGenix.DAL/Repositories/Interfaces/IGenericRepositoryFactory.cs index fb9d3da..42dd958 100644 --- a/TaskGenix.DAL/Repositories/Interfaces/IGenericRepositoryFactory.cs +++ b/TaskGenix.DAL/Repositories/Interfaces/IGenericRepositoryFactory.cs @@ -1,9 +1,8 @@ using TaskGenix.DAL.Repositories.Interfaces; -namespace TaskGenix.Repositories.Interfaces +namespace TaskGenix.Repositories.Interfaces; + +public interface IGenericRepositoryFactory { - public interface IGenericRepositoryFactory - { - IGenericRepository GetRepository() where TEntity : class; - } -} + IGenericRepository GetRepository() where TEntity : class; +} \ No newline at end of file diff --git a/TaskGenix.DAL/Repositories/Interfaces/IRecordingRepository.cs b/TaskGenix.DAL/Repositories/Interfaces/IRecordingRepository.cs new file mode 100644 index 0000000..6904112 --- /dev/null +++ b/TaskGenix.DAL/Repositories/Interfaces/IRecordingRepository.cs @@ -0,0 +1,8 @@ +using TaskGenix.Core.Domains; + +namespace TaskGenix.DAL.Repositories.Interfaces; + +public interface IRecordingRepository : IGenericRepository +{ + // Add specific methods for Recording if needed +} \ No newline at end of file diff --git a/TaskGenix.DAL/Repositories/Interfaces/ITaskAssignmentRepository.cs b/TaskGenix.DAL/Repositories/Interfaces/ITaskAssignmentRepository.cs new file mode 100644 index 0000000..4f1c53b --- /dev/null +++ b/TaskGenix.DAL/Repositories/Interfaces/ITaskAssignmentRepository.cs @@ -0,0 +1,36 @@ +using TaskGenix.Core.Domains; + +namespace TaskGenix.DAL.Repositories.Interfaces; + +public interface ITaskAssignmentRepository : IGenericRepository +{ + /// + /// Get all assignments for a specific task + /// + Task> GetAssignmentsByTaskIdAsync(Guid taskId); + + /// + /// Get all assignments for a specific user + /// + Task> GetAssignmentsByUserIdAsync(Guid userId); + + /// + /// Check if a user is already assigned to a task + /// + Task IsUserAssignedToTaskAsync(Guid taskId, Guid userId); + + /// + /// Get a specific assignment by task and user + /// + Task GetAssignmentByTaskAndUserAsync(Guid taskId, Guid userId); + + /// + /// Remove all assignments for a specific task + /// + Task RemoveAllAssignmentsForTaskAsync(Guid taskId); + + /// + /// Get assignment with user and task details + /// + Task GetAssignmentWithDetailsAsync(Guid assignmentId); +} diff --git a/TaskGenix.DAL/Repositories/Interfaces/IUnitOfWork.cs b/TaskGenix.DAL/Repositories/Interfaces/IUnitOfWork.cs index 32305d7..856626f 100644 --- a/TaskGenix.DAL/Repositories/Interfaces/IUnitOfWork.cs +++ b/TaskGenix.DAL/Repositories/Interfaces/IUnitOfWork.cs @@ -1,17 +1,16 @@ using Microsoft.EntityFrameworkCore; -namespace TaskGenix.Repositories.Interfaces -{ - public interface IUnitOfWork : IGenericRepositoryFactory, IDisposable - { - Task ExecuteInTransactionAsync(Func> operation); - Task ExecuteInTransactionAsync(Func operation); - int Commit(); - Task CommitAsync(); - } +namespace TaskGenix.Repositories.Interfaces; - public interface IUnitOfWork : IUnitOfWork where TContext : DbContext - { - TContext Context { get; } - } +public interface IUnitOfWork : IGenericRepositoryFactory, IDisposable +{ + Task ExecuteInTransactionAsync(Func> operation); + Task ExecuteInTransactionAsync(Func operation); + int Commit(); + Task CommitAsync(); } + +public interface IUnitOfWork : IUnitOfWork where TContext : DbContext +{ + TContext Context { get; } +} \ No newline at end of file diff --git a/TaskGenix.DAL/Scripts/script.sql b/TaskGenix.DAL/Scripts/script.sql index 28cd1e9..6a38b99 100644 --- a/TaskGenix.DAL/Scripts/script.sql +++ b/TaskGenix.DAL/Scripts/script.sql @@ -1,162 +1,323 @@ -- 1. Users (Base table) -INSERT INTO "TaskGenixDb"."Users" ("Id", "Username", "AvatarUrl", "Email", "PasswordHash", "Role", "LastLogin", "EmailVerified", "IsActive", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440001', 'john_doe', 'https://avatar.com/john.jpg', 'john.doe@example.com', '$2a$10$hashedpassword1', 'Admin', '2024-01-15 10:30:00+00', true, true, '2024-01-01 09:00:00', '2024-01-15 10:30:00'), -('550e8400-e29b-41d4-a716-446655440002', 'jane_smith', 'https://avatar.com/jane.jpg', 'jane.smith@example.com', '$2a$10$hashedpassword2', 'User', '2024-01-14 14:20:00+00', true, true, '2024-01-02 10:15:00', '2024-01-14 14:20:00'), -('550e8400-e29b-41d4-a716-446655440003', 'bob_wilson', NULL, 'bob.wilson@example.com', '$2a$10$hashedpassword3', 'User', '2024-01-13 16:45:00+00', true, true, '2024-01-03 11:30:00', '2024-01-13 16:45:00'), -('550e8400-e29b-41d4-a716-446655440004', 'alice_brown', 'https://avatar.com/alice.jpg', 'alice.brown@example.com', '$2a$10$hashedpassword4', 'User', '2024-01-12 08:15:00+00', true, true, '2024-01-04 12:45:00', '2024-01-12 08:15:00'), -('550e8400-e29b-41d4-a716-446655440005', 'charlie_davis', NULL, 'charlie.davis@example.com', '$2a$10$hashedpassword5', 'User', NULL, false, false, '2024-01-05 13:20:00', '2024-01-05 13:20:00'); +INSERT INTO "TaskGenixDb"."Users" ("Id", "Username", "AvatarUrl", "Email", "PasswordHash", "Role", "LastLogin", + "EmailVerified", "IsActive", "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440001', 'john_doe', 'https://avatar.com/john.jpg', 'john.doe@example.com', + '$2a$10$hashedpassword1', 'Admin', '2024-01-15 10:30:00+00', true, true, '2024-01-01 09:00:00', + '2024-01-15 10:30:00'), + ('550e8400-e29b-41d4-a716-446655440002', 'jane_smith', 'https://avatar.com/jane.jpg', 'jane.smith@example.com', + '$2a$10$hashedpassword2', 'User', '2024-01-14 14:20:00+00', true, true, '2024-01-02 10:15:00', + '2024-01-14 14:20:00'), + ('550e8400-e29b-41d4-a716-446655440003', 'bob_wilson', NULL, 'bob.wilson@example.com', '$2a$10$hashedpassword3', + 'User', '2024-01-13 16:45:00+00', true, true, '2024-01-03 11:30:00', '2024-01-13 16:45:00'), + ('550e8400-e29b-41d4-a716-446655440004', 'alice_brown', 'https://avatar.com/alice.jpg', + 'alice.brown@example.com', '$2a$10$hashedpassword4', 'User', '2024-01-12 08:15:00+00', true, true, + '2024-01-04 12:45:00', '2024-01-12 08:15:00'), + ('550e8400-e29b-41d4-a716-446655440005', 'charlie_davis', NULL, 'charlie.davis@example.com', + '$2a$10$hashedpassword5', 'User', NULL, false, false, '2024-01-05 13:20:00', '2024-01-05 13:20:00'); -- 2. Plans -INSERT INTO "TaskGenixDb"."Plans" ("Id", "Name", "Description", "Price", "DurationInDays", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440011', 'Free Plan', 'Basic features for individual users', 0.00, 365, '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440012', 'Pro Plan', 'Advanced features for professionals', 29.99, 30, '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440013', 'Team Plan', 'Collaboration features for teams', 99.99, 30, '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440014', 'Enterprise Plan', 'Full feature set for large organizations', 299.99, 365, '2024-01-01 08:00:00', '2024-01-01 08:00:00'); +INSERT INTO "TaskGenixDb"."Plans" ("Id", "Name", "Description", "Price", "DurationInDays", "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440011', 'Free Plan', 'Basic features for individual users', 0.00, 365, + '2024-01-01 08:00:00', '2024-01-01 08:00:00'), + ('550e8400-e29b-41d4-a716-446655440012', 'Pro Plan', 'Advanced features for professionals', 29.99, 30, + '2024-01-01 08:00:00', '2024-01-01 08:00:00'), + ('550e8400-e29b-41d4-a716-446655440013', 'Team Plan', 'Collaboration features for teams', 99.99, 30, + '2024-01-01 08:00:00', '2024-01-01 08:00:00'), + ('550e8400-e29b-41d4-a716-446655440014', 'Enterprise Plan', 'Full feature set for large organizations', 299.99, + 365, '2024-01-01 08:00:00', '2024-01-01 08:00:00'); -- 3. Features -INSERT INTO "TaskGenixDb"."Features" ("Id", "Key", "Name", "Unit", "FeatureType", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440021', 'projects_limit', 'Maximum Projects', 'count', 'Quota', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440022', 'tasks_per_project', 'Tasks per Project', 'count', 'Quota', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440023', 'storage_limit', 'Storage Limit', 'GB', 'Quota', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440024', 'ai_generations', 'AI Task Generations', 'count', 'Quota', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440025', 'collaboration', 'Team Collaboration', 'boolean', 'Feature', '2024-01-01 08:00:00', '2024-01-01 08:00:00'); +INSERT INTO "TaskGenixDb"."Features" ("Id", "Key", "Name", "Unit", "FeatureType", "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440021', 'projects_limit', 'Maximum Projects', 'count', 'Quota', + '2024-01-01 08:00:00', '2024-01-01 08:00:00'), + ('550e8400-e29b-41d4-a716-446655440022', 'tasks_per_project', 'Tasks per Project', 'count', 'Quota', + '2024-01-01 08:00:00', '2024-01-01 08:00:00'), + ('550e8400-e29b-41d4-a716-446655440023', 'storage_limit', 'Storage Limit', 'GB', 'Quota', '2024-01-01 08:00:00', + '2024-01-01 08:00:00'), + ('550e8400-e29b-41d4-a716-446655440024', 'ai_generations', 'AI Task Generations', 'count', 'Quota', + '2024-01-01 08:00:00', '2024-01-01 08:00:00'), + ('550e8400-e29b-41d4-a716-446655440025', 'collaboration', 'Team Collaboration', 'boolean', 'Feature', + '2024-01-01 08:00:00', '2024-01-01 08:00:00'); -- 4. PlanFeatures (Junction table between Plans and Features) -INSERT INTO "TaskGenixDb"."PlanFeatures" ("Id", "QuotaValue", "IsUnlimited", "PlanId", "FeatureId", "CreatedAt", "UpdatedAt") VALUES +INSERT INTO "TaskGenixDb"."PlanFeatures" ("Id", "QuotaValue", "IsUnlimited", "PlanId", "FeatureId", "CreatedAt", + "UpdatedAt") +VALUES -- Free Plan Features -('550e8400-e29b-41d4-a716-446655440031', 3, false, '550e8400-e29b-41d4-a716-446655440011', '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440032', 10, false, '550e8400-e29b-41d4-a716-446655440011', '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440033', 1, false, '550e8400-e29b-41d4-a716-446655440011', '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440034', 50, false, '550e8400-e29b-41d4-a716-446655440011', '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440031', 3, false, '550e8400-e29b-41d4-a716-446655440011', + '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440032', 10, false, '550e8400-e29b-41d4-a716-446655440011', + '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440033', 1, false, '550e8400-e29b-41d4-a716-446655440011', + '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440034', 50, false, '550e8400-e29b-41d4-a716-446655440011', + '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -- Pro Plan Features -('550e8400-e29b-41d4-a716-446655440035', 25, false, '550e8400-e29b-41d4-a716-446655440012', '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440036', 100, false, '550e8400-e29b-41d4-a716-446655440012', '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440037', 10, false, '550e8400-e29b-41d4-a716-446655440012', '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440038', 500, false, '550e8400-e29b-41d4-a716-446655440012', '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440035', 25, false, '550e8400-e29b-41d4-a716-446655440012', + '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440036', 100, false, '550e8400-e29b-41d4-a716-446655440012', + '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440037', 10, false, '550e8400-e29b-41d4-a716-446655440012', + '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440038', 500, false, '550e8400-e29b-41d4-a716-446655440012', + '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -- Team Plan Features -('550e8400-e29b-41d4-a716-446655440039', NULL, true, '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440040', NULL, true, '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440041', 100, false, '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440042', 2000, false, '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), -('550e8400-e29b-41d4-a716-446655440043', 1, false, '550e8400-e29b-41d4-a716-446655440013', '550e8400-e29b-41d4-a716-446655440025', '2024-01-01 08:00:00', '2024-01-01 08:00:00'); +('550e8400-e29b-41d4-a716-446655440039', NULL, true, '550e8400-e29b-41d4-a716-446655440013', + '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440040', NULL, true, '550e8400-e29b-41d4-a716-446655440013', + '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440041', 100, false, '550e8400-e29b-41d4-a716-446655440013', + '550e8400-e29b-41d4-a716-446655440023', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440042', 2000, false, '550e8400-e29b-41d4-a716-446655440013', + '550e8400-e29b-41d4-a716-446655440024', '2024-01-01 08:00:00', '2024-01-01 08:00:00'), +('550e8400-e29b-41d4-a716-446655440043', 1, false, '550e8400-e29b-41d4-a716-446655440013', + '550e8400-e29b-41d4-a716-446655440025', '2024-01-01 08:00:00', '2024-01-01 08:00:00'); -- 5. UserPlans -INSERT INTO "TaskGenixDb"."UserPlans" ("Id", "Name", "Price", "UserId", "PlanId", "StartDate", "EndDate", "IsActive", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440051', 'Pro Plan', 29.99, '550e8400-e29b-41d4-a716-446655440001', '550e8400-e29b-41d4-a716-446655440012', '2024-01-01 00:00:00+00', '2024-02-01 00:00:00+00', true, '2024-01-01 09:00:00', '2024-01-01 09:00:00'), -('550e8400-e29b-41d4-a716-446655440052', 'Free Plan', 0.00, '550e8400-e29b-41d4-a716-446655440002', '550e8400-e29b-41d4-a716-446655440011', '2024-01-02 00:00:00+00', '2025-01-02 00:00:00+00', true, '2024-01-02 10:15:00', '2024-01-02 10:15:00'), -('550e8400-e29b-41d4-a716-446655440053', 'Team Plan', 99.99, '550e8400-e29b-41d4-a716-446655440003', '550e8400-e29b-41d4-a716-446655440013', '2024-01-03 00:00:00+00', '2024-02-03 00:00:00+00', true, '2024-01-03 11:30:00', '2024-01-03 11:30:00'), -('550e8400-e29b-41d4-a716-446655440054', 'Enterprise Plan', 299.99, '550e8400-e29b-41d4-a716-446655440004', '550e8400-e29b-41d4-a716-446655440014', '2024-01-04 00:00:00+00', '2025-01-04 00:00:00+00', true, '2024-01-04 12:45:00', '2024-01-04 12:45:00'); +INSERT INTO "TaskGenixDb"."UserPlans" ("Id", "Name", "Price", "UserId", "PlanId", "StartDate", "EndDate", "IsActive", + "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440051', 'Pro Plan', 29.99, '550e8400-e29b-41d4-a716-446655440001', + '550e8400-e29b-41d4-a716-446655440012', '2024-01-01 00:00:00+00', '2024-02-01 00:00:00+00', true, + '2024-01-01 09:00:00', '2024-01-01 09:00:00'), + ('550e8400-e29b-41d4-a716-446655440052', 'Free Plan', 0.00, '550e8400-e29b-41d4-a716-446655440002', + '550e8400-e29b-41d4-a716-446655440011', '2024-01-02 00:00:00+00', '2025-01-02 00:00:00+00', true, + '2024-01-02 10:15:00', '2024-01-02 10:15:00'), + ('550e8400-e29b-41d4-a716-446655440053', 'Team Plan', 99.99, '550e8400-e29b-41d4-a716-446655440003', + '550e8400-e29b-41d4-a716-446655440013', '2024-01-03 00:00:00+00', '2024-02-03 00:00:00+00', true, + '2024-01-03 11:30:00', '2024-01-03 11:30:00'), + ('550e8400-e29b-41d4-a716-446655440054', 'Enterprise Plan', 299.99, '550e8400-e29b-41d4-a716-446655440004', + '550e8400-e29b-41d4-a716-446655440014', '2024-01-04 00:00:00+00', '2025-01-04 00:00:00+00', true, + '2024-01-04 12:45:00', '2024-01-04 12:45:00'); -- 6. UserPlanFeatures -INSERT INTO "TaskGenixDb"."UserPlanFeatures" ("Id", "QuotaTotal", "QuotaUsed", "QuotaRemaining", "IsUnlimited", "UserPlanId", "FeatureId", "CreatedAt", "UpdatedAt") VALUES +INSERT INTO "TaskGenixDb"."UserPlanFeatures" ("Id", "QuotaTotal", "QuotaUsed", "QuotaRemaining", "IsUnlimited", + "UserPlanId", "FeatureId", "CreatedAt", "UpdatedAt") +VALUES -- John's Pro Plan Features -('550e8400-e29b-41d4-a716-446655440061', 25, 5, 20, false, '550e8400-e29b-41d4-a716-446655440051', '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), -('550e8400-e29b-41d4-a716-446655440062', 100, 45, 55, false, '550e8400-e29b-41d4-a716-446655440051', '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), +('550e8400-e29b-41d4-a716-446655440061', 25, 5, 20, false, '550e8400-e29b-41d4-a716-446655440051', + '550e8400-e29b-41d4-a716-446655440021', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), +('550e8400-e29b-41d4-a716-446655440062', 100, 45, 55, false, '550e8400-e29b-41d4-a716-446655440051', + '550e8400-e29b-41d4-a716-446655440022', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), -- Jane's Free Plan Features -('550e8400-e29b-41d4-a716-446655440063', 3, 2, 1, false, '550e8400-e29b-41d4-a716-446655440052', '550e8400-e29b-41d4-a716-446655440021', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), -('550e8400-e29b-41d4-a716-446655440064', 10, 8, 2, false, '550e8400-e29b-41d4-a716-446655440052', '550e8400-e29b-41d4-a716-446655440022', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), +('550e8400-e29b-41d4-a716-446655440063', 3, 2, 1, false, '550e8400-e29b-41d4-a716-446655440052', + '550e8400-e29b-41d4-a716-446655440021', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), +('550e8400-e29b-41d4-a716-446655440064', 10, 8, 2, false, '550e8400-e29b-41d4-a716-446655440052', + '550e8400-e29b-41d4-a716-446655440022', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), -- Bob's Team Plan Features -('550e8400-e29b-41d4-a716-446655440065', 0, 15, 0, true, '550e8400-e29b-41d4-a716-446655440053', '550e8400-e29b-41d4-a716-446655440021', '2024-01-03 11:30:00', '2024-01-13 16:45:00'), -('550e8400-e29b-41d4-a716-446655440066', 0, 75, 0, true, '550e8400-e29b-41d4-a716-446655440053', '550e8400-e29b-41d4-a716-446655440022', '2024-01-03 11:30:00', '2024-01-13 16:45:00'); +('550e8400-e29b-41d4-a716-446655440065', 0, 15, 0, true, '550e8400-e29b-41d4-a716-446655440053', + '550e8400-e29b-41d4-a716-446655440021', '2024-01-03 11:30:00', '2024-01-13 16:45:00'), +('550e8400-e29b-41d4-a716-446655440066', 0, 75, 0, true, '550e8400-e29b-41d4-a716-446655440053', + '550e8400-e29b-41d4-a716-446655440022', '2024-01-03 11:30:00', '2024-01-13 16:45:00'); -- 7. PaymentTransactions -INSERT INTO "TaskGenixDb"."PaymentTransactions" ("Id", "Amount", "Currency", "TransactionDate", "PaymentMethod", "PaymentGateway", "ExternalTransactionId", "Status", "UserId", "UserPlanId", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440071', 29.99, 'USD', '2024-01-01 09:00:00', 'Credit Card', 'Stripe', 'txn_1234567890', 'Completed', '550e8400-e29b-41d4-a716-446655440001', '550e8400-e29b-41d4-a716-446655440051', '2024-01-01 09:00:00', '2024-01-01 09:00:00'), -('550e8400-e29b-41d4-a716-446655440072', 99.99, 'USD', '2024-01-03 11:30:00', 'PayPal', 'PayPal', 'PAYID-ABC123DEF', 'Completed', '550e8400-e29b-41d4-a716-446655440003', '550e8400-e29b-41d4-a716-446655440053', '2024-01-03 11:30:00', '2024-01-03 11:30:00'), -('550e8400-e29b-41d4-a716-446655440073', 299.99, 'USD', '2024-01-04 12:45:00', 'Bank Transfer', 'Stripe', 'txn_0987654321', 'Completed', '550e8400-e29b-41d4-a716-446655440004', '550e8400-e29b-41d4-a716-446655440054', '2024-01-04 12:45:00', '2024-01-04 12:45:00'); +INSERT INTO "TaskGenixDb"."PaymentTransactions" ("Id", "Amount", "Currency", "TransactionDate", "PaymentMethod", + "PaymentGateway", "ExternalTransactionId", "Status", "UserId", + "UserPlanId", "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440071', 29.99, 'USD', '2024-01-01 09:00:00', 'Credit Card', 'Stripe', + 'txn_1234567890', 'Completed', '550e8400-e29b-41d4-a716-446655440001', '550e8400-e29b-41d4-a716-446655440051', + '2024-01-01 09:00:00', '2024-01-01 09:00:00'), + ('550e8400-e29b-41d4-a716-446655440072', 99.99, 'USD', '2024-01-03 11:30:00', 'PayPal', 'PayPal', + 'PAYID-ABC123DEF', 'Completed', '550e8400-e29b-41d4-a716-446655440003', '550e8400-e29b-41d4-a716-446655440053', + '2024-01-03 11:30:00', '2024-01-03 11:30:00'), + ('550e8400-e29b-41d4-a716-446655440073', 299.99, 'USD', '2024-01-04 12:45:00', 'Bank Transfer', 'Stripe', + 'txn_0987654321', 'Completed', '550e8400-e29b-41d4-a716-446655440004', '550e8400-e29b-41d4-a716-446655440054', + '2024-01-04 12:45:00', '2024-01-04 12:45:00'); -- 8. Projects -INSERT INTO "TaskGenixDb"."Projects" ("Id", "Name", "Description", "Status", "Type", "OwnerId", "RecordingId", "DueDate", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440081', 'Website Redesign', 'Complete redesign of company website', 'InProgress', 'Web Development', '550e8400-e29b-41d4-a716-446655440001', NULL, '2024-03-01 23:59:59+00', '2024-01-05 09:00:00', '2024-01-15 10:30:00'), -('550e8400-e29b-41d4-a716-446655440082', 'Mobile App Development', 'Native iOS and Android app development', 'Planning', 'Mobile Development', '550e8400-e29b-41d4-a716-446655440002', NULL, '2024-06-01 23:59:59+00', '2024-01-06 10:15:00', '2024-01-14 14:20:00'), -('550e8400-e29b-41d4-a716-446655440083', 'Marketing Campaign Q1', 'First quarter marketing campaign planning', 'Completed', 'Marketing', '550e8400-e29b-41d4-a716-446655440003', NULL, '2024-03-31 23:59:59+00', '2024-01-07 11:30:00', '2024-01-13 16:45:00'), -('550e8400-e29b-41d4-a716-446655440084', 'Database Migration', 'Migrate from MySQL to PostgreSQL', 'OnHold', 'Database', '550e8400-e29b-41d4-a716-446655440004', NULL, '2024-04-15 23:59:59+00', '2024-01-08 12:45:00', '2024-01-12 08:15:00'); +INSERT INTO "TaskGenixDb"."Projects" ("Id", "Name", "Description", "Status", "Type", "OwnerId", "RecordingId", + "DueDate", "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440081', 'Website Redesign', 'Complete redesign of company website', + 'InProgress', 'Web Development', '550e8400-e29b-41d4-a716-446655440001', NULL, '2024-03-01 23:59:59+00', + '2024-01-05 09:00:00', '2024-01-15 10:30:00'), + ('550e8400-e29b-41d4-a716-446655440082', 'Mobile App Development', 'Native iOS and Android app development', + 'Planning', 'Mobile Development', '550e8400-e29b-41d4-a716-446655440002', NULL, '2024-06-01 23:59:59+00', + '2024-01-06 10:15:00', '2024-01-14 14:20:00'), + ('550e8400-e29b-41d4-a716-446655440083', 'Marketing Campaign Q1', 'First quarter marketing campaign planning', + 'Completed', 'Marketing', '550e8400-e29b-41d4-a716-446655440003', NULL, '2024-03-31 23:59:59+00', + '2024-01-07 11:30:00', '2024-01-13 16:45:00'), + ('550e8400-e29b-41d4-a716-446655440084', 'Database Migration', 'Migrate from MySQL to PostgreSQL', 'OnHold', + 'Database', '550e8400-e29b-41d4-a716-446655440004', NULL, '2024-04-15 23:59:59+00', '2024-01-08 12:45:00', + '2024-01-12 08:15:00'); -- 9. Recordings -INSERT INTO "TaskGenixDb"."Recordings" ("Id", "Title", "FileUrl", "Duration", "ProjectId", "UserId", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440091', 'Project Kickoff Meeting', 'https://storage.example.com/recordings/kickoff_001.mp3', '01:45:30', '550e8400-e29b-41d4-a716-446655440081', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 09:30:00+00', '2024-01-05 09:30:00+00'), -('550e8400-e29b-41d4-a716-446655440093', 'Marketing Strategy Discussion', 'https://storage.example.com/recordings/marketing_003.mp3', '02:15:45', '550e8400-e29b-41d4-a716-446655440083', '550e8400-e29b-41d4-a716-446655440003', '2024-01-08 16:00:00+00', '2024-01-08 16:00:00+00'); +INSERT INTO "TaskGenixDb"."Recordings" ("Id", "Title", "FileUrl", "Duration", "ProjectId", "UserId", "CreatedAt", + "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440091', 'Project Kickoff Meeting', + 'https://storage.example.com/recordings/kickoff_001.mp3', '01:45:30', '550e8400-e29b-41d4-a716-446655440081', + '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 09:30:00+00', '2024-01-05 09:30:00+00'), + ('550e8400-e29b-41d4-a716-446655440093', 'Marketing Strategy Discussion', + 'https://storage.example.com/recordings/marketing_003.mp3', '02:15:45', '550e8400-e29b-41d4-a716-446655440083', + '550e8400-e29b-41d4-a716-446655440003', '2024-01-08 16:00:00+00', '2024-01-08 16:00:00+00'); -- 10. TaskGens -INSERT INTO "TaskGenixDb"."TaskGens" ("Id", "Title", "Description", "Status", "Priority", "ProjectId", "DueDate", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440101', 'Create Homepage Wireframes', 'Design wireframes for the new homepage layout', 'InProgress', 'High', '550e8400-e29b-41d4-a716-446655440081', '2024-01-25 17:00:00+00', '2024-01-05 09:30:00', '2024-01-15 11:00:00'), -('550e8400-e29b-41d4-a716-446655440102', 'Implement User Authentication', 'Build login and registration functionality', 'Todo', 'High', '550e8400-e29b-41d4-a716-446655440081', '2024-02-01 17:00:00+00', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), -('550e8400-e29b-41d4-a716-446655440103', 'Setup Database Schema', 'Create and configure database tables', 'Completed', 'Medium', '550e8400-e29b-41d4-a716-446655440081', '2024-01-20 17:00:00+00', '2024-01-05 10:30:00', '2024-01-20 16:30:00'), -('550e8400-e29b-41d4-a716-446655440104', 'Mobile App Market Research', 'Research competitor apps and market trends', 'InProgress', 'Medium', '550e8400-e29b-41d4-a716-446655440082', '2024-02-15 17:00:00+00', '2024-01-06 11:00:00', '2024-01-14 15:00:00'), -('550e8400-e29b-41d4-a716-446655440105', 'Create Social Media Content', 'Develop content for Q1 social media campaign', 'Completed', 'Low', '550e8400-e29b-41d4-a716-446655440083', '2024-02-28 17:00:00+00', '2024-01-07 12:00:00', '2024-02-25 14:00:00'); +INSERT INTO "TaskGenixDb"."TaskGens" ("Id", "Title", "Description", "Status", "Priority", "ProjectId", "DueDate", + "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440101', 'Create Homepage Wireframes', + 'Design wireframes for the new homepage layout', 'InProgress', 'High', '550e8400-e29b-41d4-a716-446655440081', + '2024-01-25 17:00:00+00', '2024-01-05 09:30:00', '2024-01-15 11:00:00'), + ('550e8400-e29b-41d4-a716-446655440102', 'Implement User Authentication', + 'Build login and registration functionality', 'Todo', 'High', '550e8400-e29b-41d4-a716-446655440081', + '2024-02-01 17:00:00+00', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), + ('550e8400-e29b-41d4-a716-446655440103', 'Setup Database Schema', 'Create and configure database tables', + 'Completed', 'Medium', '550e8400-e29b-41d4-a716-446655440081', '2024-01-20 17:00:00+00', '2024-01-05 10:30:00', + '2024-01-20 16:30:00'), + ('550e8400-e29b-41d4-a716-446655440104', 'Mobile App Market Research', + 'Research competitor apps and market trends', 'InProgress', 'Medium', '550e8400-e29b-41d4-a716-446655440082', + '2024-02-15 17:00:00+00', '2024-01-06 11:00:00', '2024-01-14 15:00:00'), + ('550e8400-e29b-41d4-a716-446655440105', 'Create Social Media Content', + 'Develop content for Q1 social media campaign', 'Completed', 'Low', '550e8400-e29b-41d4-a716-446655440083', + '2024-02-28 17:00:00+00', '2024-01-07 12:00:00', '2024-02-25 14:00:00'); -- 11. TaskAssignments -INSERT INTO "TaskGenixDb"."TaskAssignments" ("Id", "TaskId", "UserId", "AssignedAt") VALUES -('550e8400-e29b-41d4-a716-446655440111', '550e8400-e29b-41d4-a716-446655440101', '550e8400-e29b-41d4-a716-446655440002', '2024-01-05 09:45:00'), -('550e8400-e29b-41d4-a716-446655440112', '550e8400-e29b-41d4-a716-446655440102', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 10:15:00'), -('550e8400-e29b-41d4-a716-446655440113', '550e8400-e29b-41d4-a716-446655440103', '550e8400-e29b-41d4-a716-446655440004', '2024-01-05 10:45:00'), -('550e8400-e29b-41d4-a716-446655440114', '550e8400-e29b-41d4-a716-446655440104', '550e8400-e29b-41d4-a716-446655440003', '2024-01-06 11:15:00'), -('550e8400-e29b-41d4-a716-446655440115', '550e8400-e29b-41d4-a716-446655440105', '550e8400-e29b-41d4-a716-446655440002', '2024-01-07 12:30:00'); +INSERT INTO "TaskGenixDb"."TaskAssignments" ("Id", "TaskId", "UserId", "AssignedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440111', '550e8400-e29b-41d4-a716-446655440101', + '550e8400-e29b-41d4-a716-446655440002', '2024-01-05 09:45:00'), + ('550e8400-e29b-41d4-a716-446655440112', '550e8400-e29b-41d4-a716-446655440102', + '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 10:15:00'), + ('550e8400-e29b-41d4-a716-446655440113', '550e8400-e29b-41d4-a716-446655440103', + '550e8400-e29b-41d4-a716-446655440004', '2024-01-05 10:45:00'), + ('550e8400-e29b-41d4-a716-446655440114', '550e8400-e29b-41d4-a716-446655440104', + '550e8400-e29b-41d4-a716-446655440003', '2024-01-06 11:15:00'), + ('550e8400-e29b-41d4-a716-446655440115', '550e8400-e29b-41d4-a716-446655440105', + '550e8400-e29b-41d4-a716-446655440002', '2024-01-07 12:30:00'); -- 12. ProjectMembers -INSERT INTO "TaskGenixDb"."ProjectMembers" ("Id", "Role", "ProjectId", "UserId", "JoinedAt", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440121', 'Owner', '550e8400-e29b-41d4-a716-446655440081', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 09:00:00', '2024-01-05 09:00:00', '2024-01-05 09:00:00'), -('550e8400-e29b-41d4-a716-446655440122', 'Developer', '550e8400-e29b-41d4-a716-446655440081', '550e8400-e29b-41d4-a716-446655440002', '2024-01-05 09:30:00', '2024-01-05 09:30:00', '2024-01-05 09:30:00'), -('550e8400-e29b-41d4-a716-446655440123', 'Designer', '550e8400-e29b-41d4-a716-446655440081', '550e8400-e29b-41d4-a716-446655440004', '2024-01-05 10:00:00', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), -('550e8400-e29b-41d4-a716-446655440124', 'Owner', '550e8400-e29b-41d4-a716-446655440082', '550e8400-e29b-41d4-a716-446655440002', '2024-01-06 10:15:00', '2024-01-06 10:15:00', '2024-01-06 10:15:00'), -('550e8400-e29b-41d4-a716-446655440125', 'Owner', '550e8400-e29b-41d4-a716-446655440083', '550e8400-e29b-41d4-a716-446655440003', '2024-01-07 11:30:00', '2024-01-07 11:30:00', '2024-01-07 11:30:00'); +INSERT INTO "TaskGenixDb"."ProjectMembers" ("Id", "Role", "ProjectId", "UserId", "JoinedAt", "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440121', 'Owner', '550e8400-e29b-41d4-a716-446655440081', + '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 09:00:00', '2024-01-05 09:00:00', '2024-01-05 09:00:00'), + ('550e8400-e29b-41d4-a716-446655440122', 'Developer', '550e8400-e29b-41d4-a716-446655440081', + '550e8400-e29b-41d4-a716-446655440002', '2024-01-05 09:30:00', '2024-01-05 09:30:00', '2024-01-05 09:30:00'), + ('550e8400-e29b-41d4-a716-446655440123', 'Designer', '550e8400-e29b-41d4-a716-446655440081', + '550e8400-e29b-41d4-a716-446655440004', '2024-01-05 10:00:00', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), + ('550e8400-e29b-41d4-a716-446655440124', 'Owner', '550e8400-e29b-41d4-a716-446655440082', + '550e8400-e29b-41d4-a716-446655440002', '2024-01-06 10:15:00', '2024-01-06 10:15:00', '2024-01-06 10:15:00'), + ('550e8400-e29b-41d4-a716-446655440125', 'Owner', '550e8400-e29b-41d4-a716-446655440083', + '550e8400-e29b-41d4-a716-446655440003', '2024-01-07 11:30:00', '2024-01-07 11:30:00', '2024-01-07 11:30:00'); -- 13. TaskReminderConfigs -INSERT INTO "TaskGenixDb"."TaskReminderConfigs" ("Id", "TaskGenId", "EnableReminders", "ReminderFrequencyInDays", "StartRemindingDaysBeforeDue", "ReminderIntervalInHours", "PreferredReminderHour", "EscalateReminderPriority", "PreferredChannel", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440131', '550e8400-e29b-41d4-a716-446655440101', true, 1, 3, 24, 9, true, 'Email', '2024-01-05 09:30:00', '2024-01-05 09:30:00'), -('550e8400-e29b-41d4-a716-446655440132', '550e8400-e29b-41d4-a716-446655440102', true, 2, 5, 48, 14, false, 'Push', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), -('550e8400-e29b-41d4-a716-446655440133', '550e8400-e29b-41d4-a716-446655440104', true, 1, 2, 12, 10, true, 'Email', '2024-01-06 11:00:00', '2024-01-06 11:00:00'); +INSERT INTO "TaskGenixDb"."TaskReminderConfigs" ("Id", "TaskGenId", "EnableReminders", "ReminderFrequencyInDays", + "StartRemindingDaysBeforeDue", "ReminderIntervalInHours", + "PreferredReminderHour", "EscalateReminderPriority", + "PreferredChannel", "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440131', '550e8400-e29b-41d4-a716-446655440101', true, 1, 3, 24, 9, true, + 'Email', '2024-01-05 09:30:00', '2024-01-05 09:30:00'), + ('550e8400-e29b-41d4-a716-446655440132', '550e8400-e29b-41d4-a716-446655440102', true, 2, 5, 48, 14, false, + 'Push', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), + ('550e8400-e29b-41d4-a716-446655440133', '550e8400-e29b-41d4-a716-446655440104', true, 1, 2, 12, 10, true, + 'Email', '2024-01-06 11:00:00', '2024-01-06 11:00:00'); -- 14. Notifications -- NotificationType: Information, Reminder, DueDate, SystemAlert, Urgent, TaskAssigned, TaskUpdated, TaskCompleted, TaskOverdue, PlanExpiring -- ReminderPattern: Once, Daily, BeforeDueDate, Custom -- DeliveryStatus: Pending, Sent, Failed -- DeliveryChannel: InApp, Email, PushNotification, SMS, All -INSERT INTO "TaskGenixDb"."Notifications" ("Id", "Title", "Message", "Type", "IsSent", "IsRead", "TaskGenId", "SequenceNumber", "ReminderPattern", "ReminderSeriesId", "ScheduledTime", "SentAt", "DeliveryStatus", "DeliveryChannel", "UserId", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440141', 'Task Due Soon', 'Your task "Create Homepage Wireframes" is due in 2 days', 'DueDate', true, false, '550e8400-e29b-41d4-a716-446655440101', 1, 'Daily', '550e8400-e29b-41d4-a716-446655440151', '2024-01-23 09:00:00+00', '2024-01-23 09:00:00+00', 'Sent', 'Email', '550e8400-e29b-41d4-a716-446655440002', '2024-01-23 08:55:00', '2024-01-23 09:00:00'), -('550e8400-e29b-41d4-a716-446655440142', 'New Task Assigned', 'You have been assigned a new task: "Implement User Authentication"', 'TaskAssigned', true, true, '550e8400-e29b-41d4-a716-446655440102', NULL, NULL, NULL, NULL, '2024-01-05 10:15:00+00', 'Sent', 'PushNotification', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 10:15:00', '2024-01-05 10:15:00'), -('550e8400-e29b-41d4-a716-446655440143', 'Task Completed', 'Task "Setup Database Schema" has been marked as completed', 'TaskCompleted', true, false, '550e8400-e29b-41d4-a716-446655440103', NULL, NULL, NULL, NULL, '2024-01-20 16:30:00+00', 'Sent', 'Email', '550e8400-e29b-41d4-a716-446655440001', '2024-01-20 16:30:00', '2024-01-20 16:30:00'), -('550e8400-e29b-41d4-a716-446655440144', 'Welcome to TaskGenix', 'Welcome to TaskGenix! Get started by creating your first project.', 'Information', true, true, NULL, NULL, NULL, NULL, NULL, '2024-01-01 09:00:00+00', 'Delivered', 'Email', '550e8400-e29b-41d4-a716-446655440001', '2024-01-01 09:00:00', '2024-01-15 10:30:00'); +INSERT INTO "TaskGenixDb"."Notifications" ("Id", "Title", "Message", "Type", "IsSent", "IsRead", "TaskGenId", + "SequenceNumber", "ReminderPattern", "ReminderSeriesId", "ScheduledTime", + "SentAt", "DeliveryStatus", "DeliveryChannel", "UserId", "CreatedAt", + "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440141', 'Task Due Soon', + 'Your task "Create Homepage Wireframes" is due in 2 days', 'DueDate', true, false, + '550e8400-e29b-41d4-a716-446655440101', 1, 'Daily', '550e8400-e29b-41d4-a716-446655440151', + '2024-01-23 09:00:00+00', '2024-01-23 09:00:00+00', 'Sent', 'Email', '550e8400-e29b-41d4-a716-446655440002', + '2024-01-23 08:55:00', '2024-01-23 09:00:00'), + ('550e8400-e29b-41d4-a716-446655440142', 'New Task Assigned', + 'You have been assigned a new task: "Implement User Authentication"', 'TaskAssigned', true, true, + '550e8400-e29b-41d4-a716-446655440102', NULL, NULL, NULL, NULL, '2024-01-05 10:15:00+00', 'Sent', + 'PushNotification', '550e8400-e29b-41d4-a716-446655440001', '2024-01-05 10:15:00', '2024-01-05 10:15:00'), + ('550e8400-e29b-41d4-a716-446655440143', 'Task Completed', + 'Task "Setup Database Schema" has been marked as completed', 'TaskCompleted', true, false, + '550e8400-e29b-41d4-a716-446655440103', NULL, NULL, NULL, NULL, '2024-01-20 16:30:00+00', 'Sent', 'Email', + '550e8400-e29b-41d4-a716-446655440001', '2024-01-20 16:30:00', '2024-01-20 16:30:00'), + ('550e8400-e29b-41d4-a716-446655440144', 'Welcome to TaskGenix', + 'Welcome to TaskGenix! Get started by creating your first project.', 'Information', true, true, NULL, NULL, + NULL, NULL, NULL, '2024-01-01 09:00:00+00', 'Delivered', 'Email', '550e8400-e29b-41d4-a716-446655440001', + '2024-01-01 09:00:00', '2024-01-15 10:30:00'); -- 15. Attachments -INSERT INTO "TaskGenixDb"."Attachments" ("Id", "FileName", "FileType", "FileSize", "Data", "TaskGenId", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440151', 'wireframe_homepage_v1.pdf', 'application/pdf', 2048576, '\x255044462d312e340a25d0d4c5d80a34203020206f626a0a3c3c0a2f4c656e6774682035203020520a2f46696c7465722f466c6174654465636f64650a3e3e0a73747265616d0a', '550e8400-e29b-41d4-a716-446655440101', '2024-01-10 14:30:00', '2024-01-10 14:30:00'), -('550e8400-e29b-41d4-a716-446655440152', 'auth_requirements.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 524288, '\x504b030414000600080000002100b7b94f4d61010000e7040000130008025b436f6e74656e745f54797065735d2e786d6c20a2040228a0000200000000000000000000', '550e8400-e29b-41d4-a716-446655440102', '2024-01-05 11:00:00', '2024-01-05 11:00:00'), -('550e8400-e29b-41d4-a716-446655440153', 'database_schema.sql', 'text/plain', 10240, '\x435245415445205441424c4520757365727320280a20206964205345524941204e4f54204e554c4c2050524956415259204b45592c0a20206e616d6520564152434841522835302920554e495155452c0a2020656d61696c20564152434841522831303029204e4f54204e554c4c0a293b', '550e8400-e29b-41d4-a716-446655440103', '2024-01-08 16:45:00', '2024-01-08 16:45:00'); +INSERT INTO "TaskGenixDb"."Attachments" ("Id", "FileName", "FileType", "FileSize", "Data", "TaskGenId", "CreatedAt", + "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440151', 'wireframe_homepage_v1.pdf', 'application/pdf', 2048576, + '\x255044462d312e340a25d0d4c5d80a34203020206f626a0a3c3c0a2f4c656e6774682035203020520a2f46696c7465722f466c6174654465636f64650a3e3e0a73747265616d0a', + '550e8400-e29b-41d4-a716-446655440101', '2024-01-10 14:30:00', '2024-01-10 14:30:00'), + ('550e8400-e29b-41d4-a716-446655440152', 'auth_requirements.docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 524288, + '\x504b030414000600080000002100b7b94f4d61010000e7040000130008025b436f6e74656e745f54797065735d2e786d6c20a2040228a0000200000000000000000000', + '550e8400-e29b-41d4-a716-446655440102', '2024-01-05 11:00:00', '2024-01-05 11:00:00'), + ('550e8400-e29b-41d4-a716-446655440153', 'database_schema.sql', 'text/plain', 10240, + '\x435245415445205441424c4520757365727320280a20206964205345524941204e4f54204e554c4c2050524956415259204b45592c0a20206e616d6520564152434841522835302920554e495155452c0a2020656d61696c20564152434841522831303029204e4f54204e554c4c0a293b', + '550e8400-e29b-41d4-a716-446655440103', '2024-01-08 16:45:00', '2024-01-08 16:45:00'); -- 16. ResourceLinks -INSERT INTO "TaskGenixDb"."ResourceLinks" ("Id", "Title", "Url", "Description", "Type", "TaskGenId", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440161', 'Figma Design System', 'https://figma.com/design-system-templates', 'Design system templates and components', 'Design', '550e8400-e29b-41d4-a716-446655440101', '2024-01-05 10:00:00', '2024-01-05 10:00:00'), -('550e8400-e29b-41d4-a716-446655440162', 'Authentication Best Practices', 'https://auth0.com/docs/best-practices', 'Comprehensive guide on authentication implementation', 'Documentation', '550e8400-e29b-41d4-a716-446655440102', '2024-01-05 10:30:00', '2024-01-05 10:30:00'), -('550e8400-e29b-41d4-a716-446655440163', 'PostgreSQL Migration Guide', 'https://postgresql.org/docs/migration', 'Step-by-step database migration documentation', 'Documentation', '550e8400-e29b-41d4-a716-446655440103', '2024-01-05 11:00:00', '2024-01-05 11:00:00'), -('550e8400-e29b-41d4-a716-446655440164', 'App Store Guidelines', 'https://developer.apple.com/app-store/guidelines/', 'Apple App Store review guidelines', 'Reference', '550e8400-e29b-41d4-a716-446655440104', '2024-01-06 11:30:00', '2024-01-06 11:30:00'), -('550e8400-e29b-41d4-a716-446655440165', 'Social Media Analytics Tool', 'https://analytics.socialmedia.com', 'Tool for tracking social media performance metrics', 'Tool', '550e8400-e29b-41d4-a716-446655440105', '2024-01-07 13:00:00', '2024-01-07 13:00:00'); +INSERT INTO "TaskGenixDb"."ResourceLinks" ("Id", "Title", "Url", "Description", "Type", "TaskGenId", "CreatedAt", + "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440161', 'Figma Design System', 'https://figma.com/design-system-templates', + 'Design system templates and components', 'Design', '550e8400-e29b-41d4-a716-446655440101', + '2024-01-05 10:00:00', '2024-01-05 10:00:00'), + ('550e8400-e29b-41d4-a716-446655440162', 'Authentication Best Practices', + 'https://auth0.com/docs/best-practices', 'Comprehensive guide on authentication implementation', + 'Documentation', '550e8400-e29b-41d4-a716-446655440102', '2024-01-05 10:30:00', '2024-01-05 10:30:00'), + ('550e8400-e29b-41d4-a716-446655440163', 'PostgreSQL Migration Guide', 'https://postgresql.org/docs/migration', + 'Step-by-step database migration documentation', 'Documentation', '550e8400-e29b-41d4-a716-446655440103', + '2024-01-05 11:00:00', '2024-01-05 11:00:00'), + ('550e8400-e29b-41d4-a716-446655440164', 'App Store Guidelines', + 'https://developer.apple.com/app-store/guidelines/', 'Apple App Store review guidelines', 'Reference', + '550e8400-e29b-41d4-a716-446655440104', '2024-01-06 11:30:00', '2024-01-06 11:30:00'), + ('550e8400-e29b-41d4-a716-446655440165', 'Social Media Analytics Tool', 'https://analytics.socialmedia.com', + 'Tool for tracking social media performance metrics', 'Tool', '550e8400-e29b-41d4-a716-446655440105', + '2024-01-07 13:00:00', '2024-01-07 13:00:00'); -- 17. UserSettings -INSERT INTO "TaskGenixDb"."UserSettings" ("Id", "ReceiveEmailNotifications", "DarkModeEnabled", "Language", "UserId", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440171', true, false, 'en-US', '550e8400-e29b-41d4-a716-446655440001', '2024-01-01 09:00:00', '2024-01-15 10:30:00'), -('550e8400-e29b-41d4-a716-446655440172', true, true, 'en-US', '550e8400-e29b-41d4-a716-446655440002', '2024-01-02 10:15:00', '2024-01-14 14:20:00'), -('550e8400-e29b-41d4-a716-446655440173', false, false, 'en-US', '550e8400-e29b-41d4-a716-446655440003', '2024-01-03 11:30:00', '2024-01-13 16:45:00'), -('550e8400-e29b-41d4-a716-446655440174', true, true, 'es-ES', '550e8400-e29b-41d4-a716-446655440004', '2024-01-04 12:45:00', '2024-01-12 08:15:00'), -('550e8400-e29b-41d4-a716-446655440175', false, false, 'en-US', '550e8400-e29b-41d4-a716-446655440005', '2024-01-05 13:20:00', '2024-01-05 13:20:00'); +INSERT INTO "TaskGenixDb"."UserSettings" ("Id", "ReceiveEmailNotifications", "DarkModeEnabled", "Language", "UserId", + "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440171', true, false, 'en-US', '550e8400-e29b-41d4-a716-446655440001', + '2024-01-01 09:00:00', '2024-01-15 10:30:00'), + ('550e8400-e29b-41d4-a716-446655440172', true, true, 'en-US', '550e8400-e29b-41d4-a716-446655440002', + '2024-01-02 10:15:00', '2024-01-14 14:20:00'), + ('550e8400-e29b-41d4-a716-446655440173', false, false, 'en-US', '550e8400-e29b-41d4-a716-446655440003', + '2024-01-03 11:30:00', '2024-01-13 16:45:00'), + ('550e8400-e29b-41d4-a716-446655440174', true, true, 'es-ES', '550e8400-e29b-41d4-a716-446655440004', + '2024-01-04 12:45:00', '2024-01-12 08:15:00'), + ('550e8400-e29b-41d4-a716-446655440175', false, false, 'en-US', '550e8400-e29b-41d4-a716-446655440005', + '2024-01-05 13:20:00', '2024-01-05 13:20:00'); -- 18. UserDeviceTokens -INSERT INTO "TaskGenixDb"."UserDeviceTokens" ("Id", "FCMToken", "DeviceId", "DeviceName", "Platform", "UserId", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440181', 'dGhpc19pc19hX21vY2tfZmNtX3Rva2VuXzEyMzQ1', 'iPhone_12_Pro_001', 'John''s iPhone', 'iOS', '550e8400-e29b-41d4-a716-446655440001', '2024-01-01 09:00:00+00', '2024-01-15 10:30:00+00'), -('550e8400-e29b-41d4-a716-446655440182', 'YW5vdGhlcl9tb2NrX2ZjbV90b2tlbl9hYmNkZWY=', 'Pixel_6_002', 'Jane''s Pixel', 'Android', '550e8400-e29b-41d4-a716-446655440002', '2024-01-02 10:15:00+00', '2024-01-14 14:20:00+00'), -('550e8400-e29b-41d4-a716-446655440183', 'bW9ja19mY21fdG9rZW5fZm9yX2JvYl93aWxzb24=', 'Galaxy_S21_003', 'Bob''s Galaxy', 'Android', '550e8400-e29b-41d4-a716-446655440003', '2024-01-03 11:30:00+00', '2024-01-13 16:45:00+00'), -('550e8400-e29b-41d4-a716-446655440184', 'YWxpY2VfYnJvd25fZmNtX3Rva2VuXzc4OTEwMTExMg==', 'MacBook_Pro_004', 'Alice''s MacBook', 'macOS', '550e8400-e29b-41d4-a716-446655440004', '2024-01-04 12:45:00+00', '2024-01-12 08:15:00+00'), -('550e8400-e29b-41d4-a716-446655440185', 'Y2hhcmxpZV9kYXZpc19mY21fdG9rZW5fMTMxNDE1MTY=', 'iPad_Air_005', 'Charlie''s iPad', 'iOS', '550e8400-e29b-41d4-a716-446655440005', '2024-01-05 13:20:00+00', '2024-01-05 13:20:00+00'); +INSERT INTO "TaskGenixDb"."UserDeviceTokens" ("Id", "FCMToken", "DeviceId", "DeviceName", "Platform", "UserId", + "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440181', 'dGhpc19pc19hX21vY2tfZmNtX3Rva2VuXzEyMzQ1', 'iPhone_12_Pro_001', + 'John''s iPhone', 'iOS', '550e8400-e29b-41d4-a716-446655440001', '2024-01-01 09:00:00+00', + '2024-01-15 10:30:00+00'), + ('550e8400-e29b-41d4-a716-446655440182', 'YW5vdGhlcl9tb2NrX2ZjbV90b2tlbl9hYmNkZWY=', 'Pixel_6_002', + 'Jane''s Pixel', 'Android', '550e8400-e29b-41d4-a716-446655440002', '2024-01-02 10:15:00+00', + '2024-01-14 14:20:00+00'), + ('550e8400-e29b-41d4-a716-446655440183', 'bW9ja19mY21fdG9rZW5fZm9yX2JvYl93aWxzb24=', 'Galaxy_S21_003', + 'Bob''s Galaxy', 'Android', '550e8400-e29b-41d4-a716-446655440003', '2024-01-03 11:30:00+00', + '2024-01-13 16:45:00+00'), + ('550e8400-e29b-41d4-a716-446655440184', 'YWxpY2VfYnJvd25fZmNtX3Rva2VuXzc4OTEwMTExMg==', 'MacBook_Pro_004', + 'Alice''s MacBook', 'macOS', '550e8400-e29b-41d4-a716-446655440004', '2024-01-04 12:45:00+00', + '2024-01-12 08:15:00+00'), + ('550e8400-e29b-41d4-a716-446655440185', 'Y2hhcmxpZV9kYXZpc19mY21fdG9rZW5fMTMxNDE1MTY=', 'iPad_Air_005', + 'Charlie''s iPad', 'iOS', '550e8400-e29b-41d4-a716-446655440005', '2024-01-05 13:20:00+00', + '2024-01-05 13:20:00+00'); -- Additional sample records for better test coverage -- More TaskGens for different projects -INSERT INTO "TaskGenixDb"."TaskGens" ("Id", "Title", "Description", "Status", "Priority", "ProjectId", "DueDate", "CreatedAt", "UpdatedAt") VALUES -('550e8400-e29b-41d4-a716-446655440106', 'API Documentation', 'Create comprehensive API documentation', 'Todo', 'Medium', '550e8400-e29b-41d4-a716-446655440081', '2024-02-10 17:00:00+00', '2024-01-16 09:00:00', '2024-01-16 09:00:00'), -('550e8400-e29b-41d4-a716-446655440107', 'UI/UX Testing', 'Conduct usability testing for mobile app', 'InProgress', 'High', '550e8400-e29b-41d4-a716-446655440082', '2024-02-20 17:00:00+00', '2024-01-17 10:00:00', '2024-01-20 15:00:00'), -('550e8400-e29b-41d4-a716-446655440108', 'Performance Optimization', 'Optimize database queries and indexing', 'Todo', 'Low', '550e8400-e29b-41d4-a716-446655440084', '2024-03-15 17:00:00+00', '2024-01-18 11:00:00', '2024-01-18 11:00:00'); +INSERT INTO "TaskGenixDb"."TaskGens" ("Id", "Title", "Description", "Status", "Priority", "ProjectId", "DueDate", + "CreatedAt", "UpdatedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440106', 'API Documentation', 'Create comprehensive API documentation', 'Todo', + 'Medium', '550e8400-e29b-41d4-a716-446655440081', '2024-02-10 17:00:00+00', '2024-01-16 09:00:00', + '2024-01-16 09:00:00'), + ('550e8400-e29b-41d4-a716-446655440107', 'UI/UX Testing', 'Conduct usability testing for mobile app', + 'InProgress', 'High', '550e8400-e29b-41d4-a716-446655440082', '2024-02-20 17:00:00+00', '2024-01-17 10:00:00', + '2024-01-20 15:00:00'), + ('550e8400-e29b-41d4-a716-446655440108', 'Performance Optimization', 'Optimize database queries and indexing', + 'Todo', 'Low', '550e8400-e29b-41d4-a716-446655440084', '2024-03-15 17:00:00+00', '2024-01-18 11:00:00', + '2024-01-18 11:00:00'); -- Additional TaskAssignments -INSERT INTO "TaskGenixDb"."TaskAssignments" ("Id", "TaskId", "UserId", "AssignedAt") VALUES -('550e8400-e29b-41d4-a716-446655440116', '550e8400-e29b-41d4-a716-446655440106', '550e8400-e29b-41d4-a716-446655440001', '2024-01-16 09:15:00'), -('550e8400-e29b-41d4-a716-446655440117', '550e8400-e29b-41d4-a716-446655440107', '550e8400-e29b-41d4-a716-446655440002', '2024-01-17 10:15:00'), -('550e8400-e29b-41d4-a716-446655440118', '550e8400-e29b-41d4-a716-446655440108', '550e8400-e29b-41d4-a716-446655440004', '2024-01-18 11:15:00'); \ No newline at end of file +INSERT INTO "TaskGenixDb"."TaskAssignments" ("Id", "TaskId", "UserId", "AssignedAt") +VALUES ('550e8400-e29b-41d4-a716-446655440116', '550e8400-e29b-41d4-a716-446655440106', + '550e8400-e29b-41d4-a716-446655440001', '2024-01-16 09:15:00'), + ('550e8400-e29b-41d4-a716-446655440117', '550e8400-e29b-41d4-a716-446655440107', + '550e8400-e29b-41d4-a716-446655440002', '2024-01-17 10:15:00'), + ('550e8400-e29b-41d4-a716-446655440118', '550e8400-e29b-41d4-a716-446655440108', + '550e8400-e29b-41d4-a716-446655440004', '2024-01-18 11:15:00'); \ No newline at end of file diff --git a/TaskGenix.DAL/TaskGenix.DAL.csproj b/TaskGenix.DAL/TaskGenix.DAL.csproj index 1356217..d770c7f 100644 --- a/TaskGenix.DAL/TaskGenix.DAL.csproj +++ b/TaskGenix.DAL/TaskGenix.DAL.csproj @@ -1,13 +1,24 @@  - - net8.0 - enable - enable - - - - - + + net8.0 + enable + enable + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/TaskGenix.Server.sln b/TaskGenix.Server.sln index f1e4be3..731db2d 100644 --- a/TaskGenix.Server.sln +++ b/TaskGenix.Server.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.12.35527.113 @@ -10,6 +11,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskGenix.DAL", "TaskGenix. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskGenix.BLL", "TaskGenix.BLL\TaskGenix.BLL.csproj", "{BE19B538-AC75-4714-A856-B6464B452345}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskGenix.Tests.Unit", "TaskGenix.Tests.Unit\TaskGenix.Tests.Unit.csproj", "{0387B7E1-B9D1-4278-9FE8-1F2CC6F8CE4F}" + ProjectSection(ProjectDependencies) = postProject + {0C7D506C-EAA9-471A-A6D6-50DF5F3AE18D} = {0C7D506C-EAA9-471A-A6D6-50DF5F3AE18D} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskGenix.Tests.Integration", "TaskGenix.Tests.Integration\TaskGenix.Tests.Integration.csproj", "{96AB3805-0970-4718-94B3-F93B47F8C920}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" + ProjectSection(SolutionItems) = preProject + README.md = README.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -32,6 +45,18 @@ Global {BE19B538-AC75-4714-A856-B6464B452345}.Debug|Any CPU.Build.0 = Debug|Any CPU {BE19B538-AC75-4714-A856-B6464B452345}.Release|Any CPU.ActiveCfg = Release|Any CPU {BE19B538-AC75-4714-A856-B6464B452345}.Release|Any CPU.Build.0 = Release|Any CPU + {0387B7E1-B9D1-4278-9FE8-1F2CC6F8CE4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0387B7E1-B9D1-4278-9FE8-1F2CC6F8CE4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0387B7E1-B9D1-4278-9FE8-1F2CC6F8CE4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0387B7E1-B9D1-4278-9FE8-1F2CC6F8CE4F}.Release|Any CPU.Build.0 = Release|Any CPU + {96AB3805-0970-4718-94B3-F93B47F8C920}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {96AB3805-0970-4718-94B3-F93B47F8C920}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96AB3805-0970-4718-94B3-F93B47F8C920}.Release|Any CPU.ActiveCfg = Release|Any CPU + {96AB3805-0970-4718-94B3-F93B47F8C920}.Release|Any CPU.Build.0 = Release|Any CPU + {65539B12-49F3-4A32-87B5-54A566E9E7C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65539B12-49F3-4A32-87B5-54A566E9E7C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65539B12-49F3-4A32-87B5-54A566E9E7C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65539B12-49F3-4A32-87B5-54A566E9E7C1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TaskGenix.Server/Controllers/AIGenerateController.cs b/TaskGenix.Server/Controllers/AIGenerateController.cs new file mode 100644 index 0000000..3a9cf3f --- /dev/null +++ b/TaskGenix.Server/Controllers/AIGenerateController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.AIModel; + +namespace TaskGenix.Server.Controllers +{ + [ApiController] + [Route("api/generate")] + public class AIGenerateController(ILogger logger, IAIGenService aIService) : BaseController(logger) + { + + [HttpPost("summarize")] + public async Task SummarizeTranscript([FromBody] CreateSummaryRequest request) + { + var result = await aIService.SummarizeTranscriptAsync(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(result)); + } + + [HttpPost("generate-tasks")] + public async Task GenerateTasks([FromBody] GenerateTasksRequest request) + { + var tasks = await aIService.GenerateTasksFromSummaryAsync(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(tasks)); + } + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/AuthController.cs b/TaskGenix.Server/Controllers/AuthController.cs new file mode 100644 index 0000000..e0f631f --- /dev/null +++ b/TaskGenix.Server/Controllers/AuthController.cs @@ -0,0 +1,121 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Exceptions; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Auth; +using TaskGenix.Core.Responses.Auth; +using TaskGenix.Core.Validators; + +namespace TaskGenix.Server.Controllers; + +[ApiController] +[Route("api/auth")] +public class AuthController(ILogger logger, IAuthService authService) + : BaseController(logger) +{ + [SwaggerOperation( + Summary = "Login user with email and password" + )] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + // [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)] + [HttpPost] + [Route("login")] + public async Task Login([FromBody] LoginRequest request) + { + if (!ModelState.IsValid) + { + var errorMessages = + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Validation failed", errorMessages)); + } + + var response = await authService.LoginAsync(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(response, "Login successful")); + } + + [SwaggerOperation( + Summary = "Register a new user", + Description = "Creates a new user account with username, email, and password." + )] + [SwaggerResponse(200, "Operation completed successfully")] + [SwaggerResponse(400, "Invalid input")] + [HttpPost] + [Route("register")] + public async Task Register([FromBody] RegisterRequest request) + { + if (!ModelState.IsValid) + { + var errorMessages = + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Validation failed", errorMessages)); + } + + await authService.RegisterAsync(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse()); + } + + [HttpPost] + [Route("logout")] + [Authorize] + public async Task Logout() + { + await authService.LogoutAsync(); + return NoContent(); + } + + [HttpPost] + [Route("refresh-token")] + [ValidateModel] + public async Task RefreshToken([FromBody] RefreshTokenRequest request) + { + var response = await authService.RefreshTokenAsync(request.RefreshToken); + return Ok(ApiResponseBuilder.BuildSuccessResponse(response)); + } + + [HttpPost] + [Route("mobile-api/login/request")] + public async Task MobileLoginRequest([FromBody] MobileLoginRequest request) + { + if (!ModelState.IsValid) + { + var errorMessages = + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Validation failed", errorMessages)); + } + + await authService.RequestOtpAsync(request.Email); + return Ok(ApiResponseBuilder.BuildSuccessResponse()); + } + + [HttpPost] + [Route("mobile-api/verify-otp")] + public async Task VerifyOtp([FromBody] VerifyOtpRequest request) + { + if (!ModelState.IsValid) + { + var errorMessages = + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Validation failed", errorMessages)); + } + + var data = await authService.VerifyOtpAsync(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(data)); + } + + [HttpPost] + [Route("mobile-api/resend-otp")] + public async Task ResendOtp([FromBody] MobileLoginRequest request) + { + if (!ModelState.IsValid) + { + var errorMessages = + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage)); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Validation failed", errorMessages)); + } + + var response = await authService.ResendOtpAsync(request.Email); + return Ok(ApiResponseBuilder.BuildSuccessResponse(response, "OTP resent successfully")); + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/BaseController.cs b/TaskGenix.Server/Controllers/BaseController.cs index e71dc7e..3819728 100644 --- a/TaskGenix.Server/Controllers/BaseController.cs +++ b/TaskGenix.Server/Controllers/BaseController.cs @@ -1,17 +1,14 @@ using Microsoft.AspNetCore.Mvc; -namespace TaskGenix.Server.Controllers -{ - [ApiController] - public class BaseController : ControllerBase where T : BaseController - { - protected ILogger _logger; - - public BaseController(ILogger logger) - { - _logger = logger; - } +namespace TaskGenix.Server.Controllers; +[ApiController] +public class BaseController : ControllerBase where T : BaseController +{ + protected ILogger _logger; + public BaseController(ILogger logger) + { + _logger = logger; } -} +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/InternalSyncController.cs b/TaskGenix.Server/Controllers/InternalSyncController.cs new file mode 100644 index 0000000..48df95f --- /dev/null +++ b/TaskGenix.Server/Controllers/InternalSyncController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Auth; + +namespace TaskGenix.Server.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class InternalSyncController(IAuthService authService) : ControllerBase +{ + [HttpPost("onboard-user")] + public async Task OnboardUser([FromBody] Auth0OnboardingRequest request) + { + try + { + var result = await authService.OnboardAuth0UserAsync(request); + + var response = ApiResponseBuilder.BuildSuccessResponse(new + { + internal_user_id = result.InternalUserId + }, "User onboarded successfully"); + + return Ok(response); + } + catch (Exception ex) + { + var errorResponse = ApiResponseBuilder.BuildErrorResponse(ex.Message); + return BadRequest(errorResponse); + } + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/NotificationController.cs b/TaskGenix.Server/Controllers/NotificationController.cs new file mode 100644 index 0000000..74c1b93 --- /dev/null +++ b/TaskGenix.Server/Controllers/NotificationController.cs @@ -0,0 +1,126 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Notification; + +namespace TaskGenix.Server.Controllers +{ + [ApiController] + [Route("api/notifications")] + public class NotificationController(ILogger logger, INotificationService _notificationService) : BaseController(logger) + { + + [SwaggerOperation(summary: "Test send a notification with fixed FCM token", description: "This endpoint is for testing purposes only.")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + [HttpGet("test/{token}")] + public async Task TestSendNotification(string token) + { + await _notificationService.SendMessageAsync(token, "hello nha hihi", "This is a test notification from TaskGenix."); + return Ok(ApiResponseBuilder.BuildSuccessResponse()); + } + + [HttpGet] + public async Task GetAllNotifications([FromQuery] int page = 1, [FromQuery] int size = 20) + { + try + { + var notifications = await _notificationService.GetAllNotificationsAsync(page, size); + return Ok(ApiResponseBuilder.BuildPageResponse( + notifications.Items, + notifications.Meta.TotalPages, + notifications.Meta.CurrentPage, + notifications.Meta.PageSize, + notifications.Meta.TotalItems, + "Notifications retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get all notifications"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve notifications", ex.Message)); + } + } + + [HttpGet("email/{email}")] + public async Task GetNotificationsByUserEmail([FromRoute] String email) + { + try + { + var notifications = await _notificationService.GetNotificationsByEmailAsync(email); + return Ok(ApiResponseBuilder.BuildSuccessResponse(notifications, "User notifications retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get notifications for user: {email}", email); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve user notifications", ex.Message)); + } + } + + [HttpPost("user/{userId:guid}")] + public async Task CreateNotification([FromRoute] Guid userId, [FromBody] CreateNotificationRequest request) + { + try + { + await _notificationService.CreateNotificationAsync(userId, request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(null, "Notification created successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create notification for user: {UserId}", userId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to create notification", ex.Message)); + } + } + + [HttpPatch("{id:guid}/read")] + public async Task MarkAsRead([FromRoute] Guid id) + { + try + { + await _notificationService.MarkAsReadAsync(id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(null, "Notification marked as read")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to mark notification as read: {NotificationId}", id); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to mark notification as read", ex.Message)); + } + } + + [HttpPost("send-to-user")] + public async Task SendNotificationToUser( + [FromQuery] string email, + [FromQuery] string title, + [FromQuery] string body, + [FromQuery] NotificationType type = NotificationType.INFO) + { + try + { + await _notificationService.SendNotificationToUserAsync(email, title, body, type); + return Ok(ApiResponseBuilder.BuildSuccessResponse(null, "Notification sent to all user devices")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send notification to user: {email}", email); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to send notification", ex.Message)); + } + } + + [HttpPost("send")] + [Obsolete("This endpoint is deprecated. Use send-to-user instead.")] + public async Task SendNotification([FromBody] SendNotificationRequest request) + { + try + { + await _notificationService.SendPushNotificationAsync(request.Token, request.Title, request.Body); + return Ok(ApiResponseBuilder.BuildSuccessResponse(null, "Notification sent!")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send push notification"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to send notification", ex.Message)); + } + } + + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/PaymentController.cs b/TaskGenix.Server/Controllers/PaymentController.cs new file mode 100644 index 0000000..bca681e --- /dev/null +++ b/TaskGenix.Server/Controllers/PaymentController.cs @@ -0,0 +1,201 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Net.payOS; +using Net.payOS.Types; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Payment; +using TaskGenix.Core.Responses.PaymentResponse; + +namespace TaskGenix.Server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class PaymentController : BaseController + { + private readonly IPaymentService _paymentService; + + private readonly PayOSConfig _payOSConfig; + + public PaymentController(IPaymentService paymentService, IOptions payOSConfig, ILogger logger) + : base(logger) + { + _paymentService = paymentService; + _payOSConfig = payOSConfig.Value; + } + + [HttpGet] + public async Task GetAllPayments() + { + try + { + _logger.LogInformation("Retrieving all payments"); + + var payments = await _paymentService.GetAllPaymentTransactionsAsync(); + return Ok(ApiResponseBuilder.BuildSuccessResponse(payments)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving payments"); + return StatusCode(500, new { message = "Có lỗi xảy ra khi lấy danh sách thanh toán" }); + } + } + + [HttpPost("create-payment-link")] + //[Authorize(Policy = "User")] + public async Task CreatePaymentLink([FromBody] PaymentRequest request) + { + try + { + _logger.LogInformation("Creating payment link for user {UserId}, amount {Amount}", request.UserId, request.Amount); + + if (!ModelState.IsValid) + { + _logger.LogWarning("Invalid payment request model state for payment link"); + return BadRequest(ModelState); + } + + var response = await _paymentService.CreatePaymentLinkAsync(request); + + if (response.Success) + { + _logger.LogInformation("Payment link created successfully for user {UserId}", request.UserId); + return Ok(response); + } + + _logger.LogWarning("Failed to create payment link for user {UserId}: {Message}", request.UserId, response.Message); + return BadRequest(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating payment link for user {UserId}", request.UserId); + return StatusCode(500, new { message = "Có lỗi xảy ra khi tạo link thanh toán" }); + } + } + + [HttpPost("webhook")] + public async Task PayOSWebhook([FromBody] object webhookData) + { + try + { + _logger.LogInformation("Received PayOS webhook: {WebhookData}", webhookData.ToString()); + + var result = await _paymentService.HandleWebhookAsync(webhookData); + + if (result.Success) + { + return Ok(new { message = "Webhook processed successfully" }); + } + + return BadRequest(new { message = result.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing PayOS webhook"); + return StatusCode(500, new { message = "Webhook processing failed" }); + } + } + [HttpGet("success")] + public async Task PaymentSuccess([FromQuery] string orderCode) + { + try + { + _logger.LogInformation("Payment success for orderCode: {OrderCode}", orderCode); + + var response = await _paymentService.HandlePaymentSuccessAsync(orderCode); + + if (response.Success) + { + return Ok(response); + } + + return BadRequest(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling payment success for orderCode: {OrderCode}", orderCode); + return StatusCode(500, new { message = "Có lỗi xảy ra khi xử lý thanh toán thành công" }); + } + } + + [HttpGet("cancel")] + public async Task CancelPayment([FromQuery] string orderCode) + { + try + { + _logger.LogInformation("Cancel payment for orderCode: {OrderCode}", orderCode); + + var response = await _paymentService.CancelPaymentAsync(orderCode); + + if (response.Success) + { + return Ok(response); + } + + return BadRequest(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error cancelling payment for orderCode: {OrderCode}", orderCode); + return StatusCode(500, new { message = "Có lỗi xảy ra khi hủy thanh toán" }); + } + } + + + [HttpPost("verify-iap")] + public async Task VerifyIap([FromBody] VerifyIapRequest request) + { + try + { + _logger.LogInformation("Verifying IAP for user {UserId}, product {ProductId}", request.UserId, request.ProductId); + + if (!ModelState.IsValid) + { + _logger.LogWarning("Invalid IAP verification request model state"); + return BadRequest(ModelState); + } + + var response = await _paymentService.VerifyIapAsync(request); + + if (response.Success) + { + _logger.LogInformation("IAP verification completed successfully for user {UserId}", request.UserId); + return Ok(response); + } + + _logger.LogWarning("Failed to verify IAP for user {UserId}: {Message}", request.UserId, response.Message); + return BadRequest(response); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error verifying IAP for user {UserId}", request.UserId); + return StatusCode(500, new { message = "Có lỗi xảy ra khi xác minh IAP" }); + } + } + + //[HttpGet("verify/{orderCode}")] + //[Authorize(Policy = "User")] + //public async Task VerifyPayment(string orderCode) + //{ + // try + // { + // _logger.LogInformation("Verifying payment for order code: {OrderCode}", orderCode); + + // var result = await _paymentService.VerifyPaymentAsync(orderCode); + + // if (result.Success) + // { + // return Ok(result); + // } + + // return BadRequest(result); + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "Error verifying payment for order code: {OrderCode}", orderCode); + // return StatusCode(500, new { message = "Có lỗi xảy ra khi xác minh thanh toán" }); + // } + //} + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/PlanController.cs b/TaskGenix.Server/Controllers/PlanController.cs index 0504f8b..cdb01f0 100644 --- a/TaskGenix.Server/Controllers/PlanController.cs +++ b/TaskGenix.Server/Controllers/PlanController.cs @@ -6,32 +6,30 @@ using TaskGenix.BLL.Services.Interfaces; using TaskGenix.Core.Meta; -namespace TaskGenix.Server.Controllers -{ - [ApiController] - [Route("api/plans")] - public class PlanController : BaseController - { - private readonly IPlanService _planService; +namespace TaskGenix.Server.Controllers; - public PlanController(ILogger logger, IPlanService planService) : base(logger) - { - _planService = planService; - } +[ApiController] +[Route("api/plans")] +public class PlanController : BaseController +{ + private readonly IPlanService _planService; - [HttpGet] - public async Task GetAllPlans() - { - var plans = await _planService.GetAllPlans(); - return Ok(ApiResponseBuilder.BuildSuccessResponse(plans)); - } + public PlanController(ILogger logger, IPlanService planService) : base(logger) + { + _planService = planService; + } - [HttpGet("{id}")] - public async Task GetPlanById(Guid id) - { - var plan = await _planService.GetPlanById(id); - return Ok(ApiResponseBuilder.BuildSuccessResponse(plan)); - } + [HttpGet] + public async Task GetAllPlans() + { + var plans = await _planService.GetAllPlans(); + return Ok(ApiResponseBuilder.BuildSuccessResponse(plans)); + } + [HttpGet("{id}")] + public async Task GetPlanById(Guid id) + { + var plan = await _planService.GetPlanById(id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(plan)); } } \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/ProjectController.cs b/TaskGenix.Server/Controllers/ProjectController.cs index 8cd44bf..2c5ced4 100644 --- a/TaskGenix.Server/Controllers/ProjectController.cs +++ b/TaskGenix.Server/Controllers/ProjectController.cs @@ -1,57 +1,156 @@ using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using TaskGenix.BLL.Services.Interfaces; using TaskGenix.Core.Domains; using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.Project; +using TaskGenix.Core.Responses.Project; +using TaskGenix.Core.Validators; -namespace TaskGenix.Server.Controllers +namespace TaskGenix.Server.Controllers; + +[ApiController] +[Route("api/projects")] +public class ProjectController : BaseController { - [ApiController] - [Route("api/projects")] - public class ProjectController : BaseController - { - private readonly IProjectService _projectService; - - public ProjectController(IProjectService projectService, ILogger logger) - : base(logger) - { - _projectService = projectService; - } - - [HttpGet] - [Route("")] - public async Task GetAllProjects() - { - var projects = await _projectService.GetAllProjectsDto(); - return Ok(ApiResponseBuilder.BuildSuccessResponse(data: projects)); - } - - [HttpGet] - [Route("{id:guid}")] - public async Task GetProjectById([FromRoute] Guid id) - { - var project = await _projectService.GetProjectByIdDto(id); - return Ok(ApiResponseBuilder.BuildSuccessResponse(data: project)); - } - - [HttpGet] - [Route("user/{userId:guid}")] - public async Task GetProjectByUserId([FromRoute] Guid userId) - { - var project = await _projectService.GetProjectsByUserIdDto(userId); - return Ok(ApiResponseBuilder.BuildSuccessResponse(data: project)); - } - - [HttpGet] - [Route("owner/{userId:guid}")] - public async Task GetProjectByOwnerId([FromRoute] Guid userId) - { - var project = await _projectService.GetProjectsByOwnerIdDto(userId); - return Ok(ApiResponseBuilder.BuildSuccessResponse(data: project)); - } + private readonly IProjectService _projectService; + + public ProjectController(IProjectService projectService, ILogger logger) + : base(logger) + { + _projectService = projectService; + } + + [SwaggerOperation( + Summary = "Get all projects", + Description = "Retrieve a list of all projects.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [HttpGet] + [Route("")] + public async Task GetAllProjects() + { + var projects = await _projectService.GetAllProjects(); + return Ok(ApiResponseBuilder.BuildSuccessResponse(projects)); + } + + [SwaggerOperation( + Summary = "Get project by ID", + Description = "Retrieve a specific project by its unique ID.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpGet] + [Route("{id:guid}")] + public async Task GetProjectById([FromRoute] Guid id) + { + var project = await _projectService.GetProjectById(id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(project)); + } + + [SwaggerOperation( + Summary = "Get projects by user ID", + Description = "Retrieve a list of projects associated with a specific user ID.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpGet] + [Route("user/{userId:guid}")] + public async Task GetProjectByUserId([FromRoute] Guid userId) + { + var projects = await _projectService.GetProjectsByUserId(userId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(projects)); + } + + [SwaggerOperation( + Summary = "Get project progress", + Description = "Get detailed progress information for a specific project.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpGet] + [Route("{id:guid}/progress")] + public async Task GetProjectProgress([FromRoute] Guid id) + { + var progress = await _projectService.GetProjectProgress(id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(progress)); + } + [SwaggerOperation( + Summary = "Get projects progress by user ID", + Description = "Get progress information for all projects associated with a user.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [HttpGet] + [Route("user/{userId:guid}/progress")] + public async Task GetProjectsProgressByUserId([FromRoute] Guid userId) + { + var progressList = await _projectService.GetProjectsProgressByUserId(userId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(progressList)); + } + + [SwaggerOperation( + Summary = "Get projects by owner ID", + Description = "Retrieve a list of projects owned by a specific user.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [HttpGet] + [Route("owner/{userId:guid}")] + public async Task GetProjectByOwnerId([FromRoute] Guid userId) + { + var projects = await _projectService.GetProjectsByOwnerId(userId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(projects)); + } + + [SwaggerOperation( + Summary = "Get projects by owner ID and status", + Description = "Retrieve a list of projects owned by a specific user filtered by status.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [HttpGet] + [Route("owner/{userId:guid}/status/{status}")] + public async Task GetProjectsByOwnerIdAndStatus([FromRoute] Guid userId, [FromRoute] ProjectStatus status) + { + var projects = await _projectService.GetProjectsByOwnerIdAndStatus(userId, status); + return Ok(ApiResponseBuilder.BuildSuccessResponse(projects)); + } + + [SwaggerOperation( + Summary = "Create a new project", + Description = "Create a new project with the provided details.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [HttpPost] + [Route("")] + [ValidateModel] + public async Task CreateProject([FromBody] CreateProjectRequest request) + { + var project = await _projectService.CreateProject(request); + return CreatedAtAction(nameof(GetProjectById), new { id = project.Id }, + ApiResponseBuilder.BuildSuccessResponse(project, "Project created successfully")); + } + + [SwaggerOperation( + Summary = "Update a project", + Description = "Update an existing project with the provided details.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpPut] + [Route("{id:guid}")] + [ValidateModel] + public async Task UpdateProject([FromRoute] Guid id, [FromBody] UpdateProjectRequest request) + { + var project = await _projectService.UpdateProject(id, request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(project, "Project updated successfully")); + } + + [SwaggerOperation( + Summary = "Delete a project", + Description = "Soft delete a project by setting IsActive to false.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpDelete] + [Route("{id:guid}")] + public async Task DeleteProject([FromRoute] Guid id) + { + await _projectService.DeleteProject(id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(new { success = true }, "Project deleted successfully")); } } \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/ProjectMemberController.cs b/TaskGenix.Server/Controllers/ProjectMemberController.cs new file mode 100644 index 0000000..0d3ea2e --- /dev/null +++ b/TaskGenix.Server/Controllers/ProjectMemberController.cs @@ -0,0 +1,89 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.ProjectMember; +using TaskGenix.Core.Responses.Project; +using TaskGenix.Core.Validators; + +namespace TaskGenix.Server.Controllers; + +[ApiController] +[Route("api/projects/{projectId:guid}/members")] +public class ProjectMemberController : BaseController +{ + private readonly IProjectMemberService _projectMemberService; + + public ProjectMemberController(IProjectMemberService projectMemberService, ILogger logger) + : base(logger) + { + _projectMemberService = projectMemberService; + } + + [SwaggerOperation( + Summary = "Get all members of a project", + Description = "Retrieve a list of all members associated with a specific project.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpGet] + [Route("")] + public async Task GetProjectMembers([FromRoute] Guid projectId) + { + var members = await _projectMemberService.GetMembersByProjectId(projectId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(members)); + } + + + [SwaggerOperation( + Summary = "Add a member to a project", + Description = "Add a new member to the specified project with a role.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status409Conflict)] + [HttpPost] + [Route("")] + [ValidateModel] + public async Task AddMemberToProject([FromRoute] Guid projectId, + [FromBody] AddProjectMemberRequest request) + { + var member = await _projectMemberService.AddMemberToProject(projectId, request); + return CreatedAtAction(nameof(GetProjectMembers), + new { projectId = projectId }, + ApiResponseBuilder.BuildSuccessResponse(member, "Member added to project successfully")); + } + + [SwaggerOperation( + Summary = "Update a project member", + Description = "Update the role of an existing project member.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpPut] + [Route("{userId:guid}")] + [ValidateModel] + public async Task UpdateProjectMember([FromRoute] Guid projectId, [FromRoute] Guid userId, + [FromBody] UpdateProjectMemberRequest request) + { + var member = await _projectMemberService.UpdateProjectMember(projectId, userId, request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(member, "Project member updated successfully")); + } + + [SwaggerOperation( + Summary = "Remove a member from a project", + Description = "Remove an existing member from the specified project.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpDelete] + [Route("{userId:guid}")] + public async Task RemoveMemberFromProject([FromRoute] Guid projectId, [FromRoute] Guid userId) + { + await _projectMemberService.RemoveMemberFromProject(projectId, userId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(new { success = true }, + "Member removed from project successfully")); + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/RecordingController.cs b/TaskGenix.Server/Controllers/RecordingController.cs new file mode 100644 index 0000000..6ab5784 --- /dev/null +++ b/TaskGenix.Server/Controllers/RecordingController.cs @@ -0,0 +1,134 @@ +using Microsoft.AspNetCore.Mvc; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Extensions; +using TaskGenix.Core.Requests.Recording; +using TaskGenix.Core.Responses.Recording; +using TaskGenix.Server.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class RecordingController : BaseController +{ + private readonly IRecordingService _recordingService; + private readonly IFileAccessService _fileAccessService; + + public RecordingController(IRecordingService recordingService, IFileAccessService fileAccessService, ILogger logger) + : base(logger) + { + _recordingService = recordingService; + _fileAccessService = fileAccessService; + } + + [HttpPost("upload")] + //[ValidateFile(25, new[] { ".mp3", ".wav", ".m4a" }, false, "file")] + public async Task Upload([FromForm] UploadRecordingRequest request) + { + if (request.File == null || request.File.Length == 0) + return BadRequest("No file uploaded."); + + using var fileStream = request.File.OpenReadStream(); + + var response = await _recordingService.UploadRecordingAsync( + fileStream, + request.File.Length, + request.Title, + request.OwnerEmail, + request.File.ContentType, + request.Duration, + request.TranscriptText + ); + + return CreatedAtAction(nameof(Get), new { id = response.Id }, response); + } + + [HttpGet("{id}")] + public async Task Get(Guid id) + { + var meta = await _recordingService.GetRecordingMetaAsync(id); + if (meta == null) return NotFound(); + return Ok(meta); + } + + [HttpGet("{id}/stream-url")] + public async Task GetStreamUrl(Guid id) + { + try + { + var recording = await _recordingService.GetRecordingByIdAsync(id); + if (recording == null) + return NotFound(); + + var downloadUrl = await _fileAccessService.GeneratePresignedUrlAsync( + recording, + TimeSpan.FromHours(1) + ); + + return Ok(new + { + DownloadUrl = downloadUrl, + FileName = $"{recording.Title}{Path.GetExtension(recording.ObjectKey)}", + FileSize = recording.FileSize, + ExpiresAt = DateTimeOffset.UtcNow.AddHours(1) + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to generate download URL for recording {RecordingId}", id); + return StatusCode(500, "Failed to generate download URL"); + } + } + + [HttpGet] + public async Task GetAll() + { + var recordings = await _recordingService.GetAllRecordings(); + return Ok(recordings); + } + + [HttpPost] + public async Task Create([FromBody] CreateRecordingRequest request) + { + var response = await _recordingService.CreateRecording(request); + return CreatedAtAction(nameof(Get), new { id = response.Id }, response); + } + + [HttpPut("{id}")] + public async Task Update(Guid id, [FromBody] UpdateRecordingRequest request) + { + var response = await _recordingService.UpdateRecording(id, request); + return Ok(response); + } + + [HttpGet("{id}/stream")] + public async Task Stream(Guid id) + { + try + { + var recording = await _recordingService.GetRecordingByIdAsync(id); + if (recording == null) + return NotFound(); + + // TODO: Add authorization check + // if (!await CanUserAccessRecording(User, recording)) + // return Forbid(); + + var fileStream = await _fileAccessService.GetFileStreamAsync(recording); + var contentType = recording.MimeType ?? "application/octet-stream"; + var fileName = $"{recording.Title}{Path.GetExtension(recording.ObjectKey)}"; + + return File(fileStream, contentType, fileName, enableRangeProcessing: true); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to stream recording {RecordingId}", id); + return StatusCode(500, "Failed to stream recording"); + } + } + + [HttpGet("user/{userId}")] + public async Task GetByUserId(Guid userId) + { + var recordings = await _recordingService.GetRecordingsByUserIdAsync(userId); + return Ok(recordings); + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/SystemStatusController.cs b/TaskGenix.Server/Controllers/SystemStatusController.cs new file mode 100644 index 0000000..bbef314 --- /dev/null +++ b/TaskGenix.Server/Controllers/SystemStatusController.cs @@ -0,0 +1,129 @@ +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Responses.SystemStatus; + +namespace TaskGenix.Server.Controllers; + +/// +/// System status and health check endpoints +/// +[ApiController] +[Route("api/system-status")] +public class SystemStatusController : BaseController +{ + private readonly ISystemStatusService _systemStatusService; + + public SystemStatusController( + ILogger logger, + ISystemStatusService systemStatusService) + : base(logger) + { + _systemStatusService = systemStatusService; + } + + /// + /// Get overall system status including all services + /// + [HttpGet] + [SwaggerOperation( + Summary = "Get system status", + Description = "Returns the current operational status of all system services and components")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + public async Task GetSystemStatus() + { + try + { + _logger.LogInformation("Fetching overall system status"); + var status = await _systemStatusService.GetSystemStatusAsync(); + if (status.OverallStatus.Contains("Major")) + return StatusCode(503, status); + return Ok(ApiResponseBuilder.BuildSuccessResponse(status, "System status retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get system status"); + return StatusCode(500, ApiResponseBuilder.BuildErrorResponse( + "Failed to retrieve system status", + ex.Message)); + } + } + + /// + /// Check status of a specific service + /// + [HttpGet("service/{serviceName}")] + [SwaggerOperation( + Summary = "Get specific service status", + Description = "Returns the operational status of a specific service by name")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + public async Task GetServiceStatus([FromRoute] string serviceName) + { + try + { + _logger.LogInformation("Checking status for service: {ServiceName}", serviceName); + var status = await _systemStatusService.CheckServiceStatusAsync(serviceName); + + if (status.Status == ServiceHealthStatus.MinorOutage && status.Message == "Service not found") + { + return NotFound(ApiResponseBuilder.BuildErrorResponse( + "Service not found", + $"Service '{serviceName}' does not exist")); + } + + return Ok(ApiResponseBuilder.BuildSuccessResponse(status, "Service status retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get service status for {ServiceName}", serviceName); + return StatusCode(500, ApiResponseBuilder.BuildErrorResponse( + "Failed to retrieve service status", + ex.Message)); + } + } + + /// + /// Get list of all monitored services + /// + [HttpGet("services")] + [SwaggerOperation( + Summary = "Get available services", + Description = "Returns a list of all services being monitored")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + public async Task GetAvailableServices() + { + try + { + _logger.LogInformation("Fetching list of available services"); + var services = await _systemStatusService.GetAvailableServicesAsync(); + return Ok(ApiResponseBuilder.BuildSuccessResponse(services, "Available services retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get available services"); + return StatusCode(500, ApiResponseBuilder.BuildErrorResponse( + "Failed to retrieve available services", + ex.Message)); + } + } + + /// + /// Simple health check endpoint (for load balancers, monitoring tools) + /// + [HttpGet("health")] + [SwaggerOperation( + Summary = "Simple health check", + Description = "Returns a simple OK response if the API is running. Used by load balancers and monitoring tools.")] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + public IActionResult HealthCheck() + { + return Ok(new + { + status = "healthy", + timestamp = DateTime.UtcNow, + service = "TaskGenix API" + }); + } +} diff --git a/TaskGenix.Server/Controllers/TaskAssignmentController.cs b/TaskGenix.Server/Controllers/TaskAssignmentController.cs new file mode 100644 index 0000000..521bc23 --- /dev/null +++ b/TaskGenix.Server/Controllers/TaskAssignmentController.cs @@ -0,0 +1,214 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.TaskAssignment; +using TaskGenix.Core.Validators; + +namespace TaskGenix.Server.Controllers; + +[ApiController] +[Route("api/task-assignments")] +[Authorize] +public class TaskAssignmentController : BaseController +{ + private readonly ITaskAssignmentService _taskAssignmentService; + + public TaskAssignmentController( + ILogger logger, + ITaskAssignmentService taskAssignmentService) + : base(logger) + { + _taskAssignmentService = taskAssignmentService; + } + + /// + /// Assign a user to a task + /// + [HttpPost("assign")] + [ValidateModel] + public async Task AssignUserToTask([FromBody] AssignUserToTaskRequest request) + { + try + { + var response = await _taskAssignmentService.AssignUserToTaskAsync(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(response, "User successfully assigned to task")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to assign user {UserId} to task {TaskId}", request.UserId, request.TaskId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to assign user to task", ex.Message)); + } + } + + /// + /// Assign multiple users to a task at once + /// + [HttpPost("bulk-assign")] + [ValidateModel] + public async Task BulkAssignUsersToTask([FromBody] BulkAssignUsersRequest request) + { + try + { + var responses = await _taskAssignmentService.BulkAssignUsersToTaskAsync(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(responses, + $"Successfully assigned {responses.Count} user(s) to task")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to bulk assign users to task {TaskId}", request.TaskId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to bulk assign users to task", ex.Message)); + } + } + + /// + /// Remove a user from a task + /// + [HttpPost("remove")] + [ValidateModel] + public async Task RemoveUserFromTask([FromBody] RemoveUserFromTaskRequest request) + { + try + { + var result = await _taskAssignmentService.RemoveUserFromTaskAsync(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(result, "User successfully removed from task")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove user {UserId} from task {TaskId}", request.UserId, request.TaskId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to remove user from task", ex.Message)); + } + } + + /// + /// Get all assignments for a specific task + /// + [HttpGet("task/{taskId:guid}")] + public async Task GetAssignmentsByTaskId([FromRoute] Guid taskId) + { + try + { + var assignments = await _taskAssignmentService.GetAssignmentsByTaskIdAsync(taskId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(assignments, + $"Retrieved {assignments.Count} assignment(s) for task")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get assignments for task {TaskId}", taskId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve task assignments", ex.Message)); + } + } + + /// + /// Get all assignments for a specific user + /// + [HttpGet("user/{userId:guid}")] + public async Task GetAssignmentsByUserId([FromRoute] Guid userId) + { + try + { + var assignments = await _taskAssignmentService.GetAssignmentsByUserIdAsync(userId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(assignments, + $"Retrieved {assignments.Count} assignment(s) for user")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get assignments for user {UserId}", userId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve user assignments", ex.Message)); + } + } + + /// + /// Get all assignments for the current logged-in user + /// + [HttpGet("my-assignments")] + public async Task GetMyAssignments() + { + try + { + var assignments = await _taskAssignmentService.GetMyAssignmentsAsync(); + return Ok(ApiResponseBuilder.BuildSuccessResponse(assignments, + $"Retrieved {assignments.Count} assignment(s)")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get current user's assignments"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve your assignments", ex.Message)); + } + } + + /// + /// Get a specific assignment by ID + /// + [HttpGet("{assignmentId:guid}")] + public async Task GetAssignmentById([FromRoute] Guid assignmentId) + { + try + { + var assignment = await _taskAssignmentService.GetAssignmentByIdAsync(assignmentId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(assignment, "Assignment retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get assignment {AssignmentId}", assignmentId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve assignment", ex.Message)); + } + } + + /// + /// Check if a user is assigned to a task + /// + [HttpGet("check")] + public async Task IsUserAssignedToTask([FromQuery] Guid taskId, [FromQuery] Guid userId) + { + try + { + var isAssigned = await _taskAssignmentService.IsUserAssignedToTaskAsync(taskId, userId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(new { IsAssigned = isAssigned }, + isAssigned ? "User is assigned to task" : "User is not assigned to task")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to check if user {UserId} is assigned to task {TaskId}", userId, taskId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to check assignment status", ex.Message)); + } + } + + /// + /// Remove all assignments for a task (Admin only) + /// + [HttpDelete("task/{taskId:guid}/all")] + [Authorize(Roles = "Admin")] + public async Task RemoveAllAssignmentsForTask([FromRoute] Guid taskId) + { + try + { + var result = await _taskAssignmentService.RemoveAllAssignmentsForTaskAsync(taskId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(result, "All assignments removed successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to remove all assignments for task {TaskId}", taskId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to remove all assignments", ex.Message)); + } + } + + /// + /// Get all available users (project members) that can be assigned to a task + /// + [HttpGet("task/{taskId:guid}/available-users")] + public async Task GetAvailableUsersForTask([FromRoute] Guid taskId) + { + try + { + var users = await _taskAssignmentService.GetAvailableUsersForTaskAsync(taskId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(users, + $"Retrieved {users.Count} available user(s) for task")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get available users for task {TaskId}", taskId); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve available users", ex.Message)); + } + } +} diff --git a/TaskGenix.Server/Controllers/TaskGenController.cs b/TaskGenix.Server/Controllers/TaskGenController.cs index 8ff7224..a428e16 100644 --- a/TaskGenix.Server/Controllers/TaskGenController.cs +++ b/TaskGenix.Server/Controllers/TaskGenController.cs @@ -1,57 +1,163 @@ using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TaskGenix.BLL.Services.Interfaces; - using TaskGenix.Core.Meta; using TaskGenix.Core.Requests.TaskGen; +using TaskGenix.Core.Responses.TaskGen; +using TaskGenix.Core.Validators; + +namespace TaskGenix.Server.Controllers; -namespace TaskGenix.Server.Controllers +[ApiController] +[Route("api/taskgens")] +public class TaskGenController : BaseController { - [ApiController] - [Route("api/taskgens")] - public class TaskGenController : BaseController - { - private readonly ITaskGenService _taskGenService; - - public TaskGenController(ITaskGenService taskGenService, ILogger logger) - : base(logger) - { - _taskGenService = taskGenService; - } - - [HttpGet] - [Route("")] - public async Task GetAllTasks() - { - var tasks = await _taskGenService.GetAllTasksDto(); - return Ok(ApiResponseBuilder.BuildSuccessResponse(data: tasks)); - } - - [HttpGet] - [Route("user/{userId:guid}")] - public async Task GetAllTasksByUserId([FromRoute] Guid userId) - { - var tasks = await _taskGenService.GetTasksByUserIdDto(userId); - return Ok(ApiResponseBuilder.BuildSuccessResponse(data: tasks)); - } - - [HttpGet] - [Route("project/{projectId:guid}")] - public async Task GetAllTasksByProjectId([FromRoute] Guid projectId) - { - var tasks = await _taskGenService.GetTasksByProjectIdDto(projectId); - return Ok(ApiResponseBuilder.BuildSuccessResponse(data: tasks)); - } - - [HttpGet] - [Route("{taskId:guid}")] - public async Task GetTaskById([FromRoute] Guid taskId) - { - var task = await _taskGenService.GetTaskByIdDto(taskId); - return Ok(ApiResponseBuilder.BuildSuccessResponse(data: task)); - } + private readonly ITaskGenService _taskGenService; + + public TaskGenController(ITaskGenService taskGenService, ILogger logger) + : base(logger) + { + _taskGenService = taskGenService; + } + + [SwaggerOperation( + Summary = "Get all tasks", + Description = "Retrieve a list of all tasks in the system.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [HttpGet] + [Route("")] + public async Task GetAllTasks() + { + var tasks = await _taskGenService.GetAllTasks(); + return Ok(ApiResponseBuilder.BuildSuccessResponse(tasks)); + } + + [SwaggerOperation( + Summary = "Get tasks by user ID", + Description = "Retrieve all tasks assigned to a specific user.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [HttpGet] + [Route("user/{userId:guid}")] + public async Task GetAllTasksByUserId([FromRoute] Guid userId) + { + var tasks = await _taskGenService.GetTasksByUserId(userId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(tasks)); + } + + [SwaggerOperation( + Summary = "Get tasks by project ID", + Description = "Retrieve all tasks associated with a specific project.")] + [ProducesResponseType(typeof(SuccessResponse>), StatusCodes.Status200OK)] + [HttpGet] + [Route("project/{projectId:guid}")] + public async Task GetAllTasksByProjectId([FromRoute] Guid projectId) + { + var tasks = await _taskGenService.GetTasksByProjectId(projectId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(tasks)); + } + + [SwaggerOperation( + Summary = "Get task by ID", + Description = "Retrieve a specific task by its unique ID.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpGet] + [Route("{taskId:guid}")] + public async Task GetTaskById([FromRoute] Guid taskId) + { + var task = await _taskGenService.GetTaskById(taskId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(task)); + } + + [SwaggerOperation( + Summary = "Create a new task", + Description = "Create a new task with the provided details.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [HttpPost] + [Route("")] + [ValidateModel] + public async Task CreateTask([FromBody] CreateTaskRequest request) + { + var task = await _taskGenService.CreateTask(request); + var response = await _taskGenService.GetTaskById(task.Id); + return CreatedAtAction(nameof(GetTaskById), new { taskId = task.Id }, + ApiResponseBuilder.BuildSuccessResponse(response, "Task created successfully")); + } + + [SwaggerOperation( + Summary = "Create multiple tasks", + Description = "Create multiple tasks in bulk.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [HttpPost] + [Route("bulk")] + [ValidateModel] + public async Task CreateTasksBulk([FromBody] IEnumerable requests) + { + var tasks = await _taskGenService.CreateTaskBulk(requests); + var taskIds = tasks.Select(t => t.Id).ToList(); + return Ok(ApiResponseBuilder.BuildSuccessResponse( + new { createdTaskIds = taskIds, count = taskIds.Count }, + $"{taskIds.Count} tasks created successfully")); + } + + [SwaggerOperation( + Summary = "Update a task", + Description = "Update an existing task with the provided details.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpPut] + [Route("{taskId:guid}")] + [ValidateModel] + public async Task UpdateTask([FromRoute] Guid taskId, [FromBody] UpdateTaskRequest request) + { + var task = await _taskGenService.UpdateTask(taskId, request); + var response = await _taskGenService.GetTaskById(task.Id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(response, "Task updated successfully")); + } + + [SwaggerOperation( + Summary = "Delete a task", + Description = "Delete an existing task and its assignments.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpDelete] + [Route("{taskId:guid}")] + public async Task DeleteTask([FromRoute] Guid taskId) + { + await _taskGenService.DeleteTask(taskId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(new { success = true }, "Task deleted successfully")); + } + + [SwaggerOperation( + Summary = "Assign task to project", + Description = "Assign an existing task to a specific project.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpPut] + [Route("{taskId:guid}/assign-to-project/{projectId:guid}")] + public async Task AssignTaskToProject([FromRoute] Guid taskId, [FromRoute] Guid projectId) + { + var task = await _taskGenService.AssignTaskToProject(taskId, projectId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(task, "Task assigned to project successfully")); + } + + [SwaggerOperation( + Summary = "Unassign task from project", + Description = "Remove a task's association with its current project.")] + [ProducesResponseType(typeof(SuccessResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpPut] + [Route("{taskId:guid}/unassign-from-project")] + public async Task UnassignTaskFromProject([FromRoute] Guid taskId) + { + var task = await _taskGenService.UnassignTaskFromProject(taskId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(task, "Task unassigned from project successfully")); } } \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/UserController.cs b/TaskGenix.Server/Controllers/UserController.cs index 7e20101..572487b 100644 --- a/TaskGenix.Server/Controllers/UserController.cs +++ b/TaskGenix.Server/Controllers/UserController.cs @@ -3,39 +3,125 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; using TaskGenix.BLL.Services.Interfaces; using TaskGenix.Core.Meta; +using TaskGenix.Core.Responses.User; +using TaskGenix.Core.Validators; -namespace TaskGenix.Server.Controllers +namespace TaskGenix.Server.Controllers; + +[ApiController] +[Route("api/users")] +public class UserController(ILogger logger, IUserService userService) + : BaseController(logger) { - [ApiController] - [Route("api/users")] - public class UserController : BaseController - { - private readonly IUserService _userService; - - public UserController(ILogger logger, IUserService userService) : base(logger) - { - _userService = userService; - } - - [HttpGet] - public async Task GetAllUsers() - { - var users = await _userService.GetAllUsers(); - return Ok(ApiResponseBuilder.BuildSuccessResponse(users)); - } - - [HttpGet("{id}")] - public async Task GetUserById(Guid id) - { - var user = await _userService.GetUserById(id); - if (user == null) - { - return NotFound(ApiResponseBuilder.BuildErrorResponse("User not found")); - } - return Ok(ApiResponseBuilder.BuildSuccessResponse(user)); - } + [SwaggerOperation( + Summary = "Get all users with pagination", + Description = "Retrieve a paginated list of all users in the system.")] + [ProducesResponseType(typeof(ApiResponse>>), StatusCodes.Status200OK)] + [HttpGet("paged")] + public async Task GetAllUsersPaged([FromQuery] int page = 1, [FromQuery] int pageSize = 10) + { + var pagedResult = await userService.GetUsersPaged(page, pageSize); + var response = ApiResponseBuilder.BuildPageResponse( + pagedResult.Items, // items + pagedResult.Meta.TotalPages, // totalPages + pagedResult.Meta.CurrentPage, // currentPage + pagedResult.Meta.PageSize, // pageSize + pagedResult.Meta.TotalItems // totalItems + ); + + return Ok(response); + } + + [Obsolete("This method will be removed in v2.0. Please use GetAllUsersPaged instead.")] + [HttpGet] + public async Task GetAllUsers() + { + var users = await userService.GetAllUsers(); + return Ok(ApiResponseBuilder.BuildSuccessResponse(users)); + } + + [SwaggerOperation( + Summary = "Get user by ID", + Description = "Retrieve a user by their unique ID.")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status422UnprocessableEntity)] + [HttpGet("{id}")] + public async Task GetUserById(Guid id) + { + var user = await userService.GetUserById(id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(user)); + } + + + [SwaggerOperation( + Summary = "Get user by Auth0ID", + Description = "Retrieve a user by their unique auth0 ID.")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status422UnprocessableEntity)] + [HttpGet("auth0/{auth0Id}")] + public async Task GetUserByAuth0Id(string auth0id) + { + var user = await userService.GetUserByAuth0Id(auth0id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(user)); + } + + [SwaggerOperation( + Summary = "Create a new user", + Description = "Create a new user with the provided details.")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [HttpPost] + [ValidateModel] + public async Task CreateUser([FromBody] UserRequest user) + { + var createdUser = await userService.CreateUser(user); + return Ok(ApiResponseBuilder.BuildSuccessResponse(createdUser)); + } + + [SwaggerOperation( + Summary = "Update an existing user (full replacement)", + Description = + "Update all fields of an existing user by their unique ID. All fields are required for PUT operation.")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpPut("{id}")] + [ValidateModel] + public async Task UpdateUser(Guid id, [FromBody] UpdateUserRequest user) + { + var updatedUser = await userService.UpdateUser(id, user); + return Ok(ApiResponseBuilder.BuildSuccessResponse(updatedUser)); + } + + [SwaggerOperation( + Summary = "Partially update an existing user", + Description = + "Update only specified fields of an existing user by their unique ID. All fields are optional for PATCH operation.")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status404NotFound)] + [HttpPatch("{id}")] + [ValidateModel] + public async Task UpdatePartialUser(Guid id, [FromBody] UpdatePartialUserRequest user) + { + var updatedUser = await userService.UpdatePartialUser(id, user); + return Ok(ApiResponseBuilder.BuildSuccessResponse(updatedUser)); + } + + [SwaggerOperation( + Summary = "Delete a user", + Description = "Delete a user by their unique ID.")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ErrorResponse), StatusCodes.Status400BadRequest)] + [HttpDelete("{id}")] + public async Task DeleteUser(Guid id) + { + return Ok(ApiResponseBuilder.BuildSuccessResponse(await userService.DeleteUser(id))); } } \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/UserDeviceTokenController.cs b/TaskGenix.Server/Controllers/UserDeviceTokenController.cs new file mode 100644 index 0000000..b849cf8 --- /dev/null +++ b/TaskGenix.Server/Controllers/UserDeviceTokenController.cs @@ -0,0 +1,232 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.UserDeviceToken; +using TaskGenix.Core.Validators; + +namespace TaskGenix.Server.Controllers +{ + [ApiController] + [Route("api/fcm-tokens")] + [Authorize] + public class UserDeviceTokenController : BaseController + { + private readonly IUserDeviceTokenService _userDeviceTokenService; + private readonly IAuthService _authService; + + public UserDeviceTokenController( + ILogger logger, + IUserDeviceTokenService userDeviceTokenService, + IAuthService authService) + : base(logger) + { + _userDeviceTokenService = userDeviceTokenService; + _authService = authService; + } + + [HttpGet] + public async Task GetAllTokens([FromQuery] int page = 1, [FromQuery] int size = 20) + { + try + { + var tokens = await _userDeviceTokenService.GetAllTokensAsync(page, size); + return Ok(ApiResponseBuilder.BuildPageResponse( + tokens.Items, + tokens.Meta.TotalPages, + tokens.Meta.CurrentPage, + tokens.Meta.PageSize, + tokens.Meta.TotalItems, + "Device tokens retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get all device tokens"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve device tokens", ex.Message)); + } + } + + [HttpGet("{id:guid}")] + public async Task GetTokenById([FromRoute] Guid id) + { + try + { + var token = await _userDeviceTokenService.GetTokenByIdAsync(id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(token, "Device token retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get device token: {TokenId}", id); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve device token", ex.Message)); + } + } + + [HttpPost] + [ValidateModel] + public async Task CreateOrUpdateToken([FromBody] CreateUserDeviceTokenRequestV2 request) + { + try + { + // Let the service handle getting current user email from JWT token + var response = await _userDeviceTokenService.CreateOrUpdateTokenAsyncV2(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(response, "Device token created/updated successfully")); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogError(ex, "Unauthorized access when creating/updating device token"); + return Unauthorized(ApiResponseBuilder.BuildErrorResponse("Authentication required", ex.Message)); + } + catch (ArgumentException ex) + { + _logger.LogError(ex, "Invalid arguments when creating/updating device token"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Invalid request data", ex.Message)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create/update device token"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to create/update device token", ex.Message)); + } + } + + [HttpPost("register-with-email")] + [ValidateModel] + public async Task RegisterTokenWithEmail([FromBody] CreateUserDeviceTokenRequestV2 request) + { + try + { + // Extract email from the request body rather than JWT for this endpoint + var response = await _userDeviceTokenService.CreateOrUpdateTokenWithEmailAsync(request.Email, request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(response, "Device token registered successfully via email")); + } + catch (ArgumentException ex) + { + _logger.LogError(ex, "Invalid arguments when registering device token with email"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Invalid request data", ex.Message)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to register device token with email: {Email}", request?.Email ?? "unknown"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to register device token", ex.Message)); + } + } + + [HttpPost("cleanup-expired")] + [Authorize(Roles = "Admin")] // Only allow admin to cleanup + public async Task CleanupExpiredTokens([FromQuery] int olderThanDays = 90) + { + try + { + var cleanedCount = await _userDeviceTokenService.CleanupExpiredTokensAsync(olderThanDays); + return Ok(ApiResponseBuilder.BuildSuccessResponse(new { CleanedTokensCount = cleanedCount }, + $"Cleaned up {cleanedCount} expired FCM tokens")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to cleanup expired FCM tokens"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to cleanup expired tokens", ex.Message)); + } + } + + [HttpPatch("{id:guid}")] + public async Task UpdateToken([FromRoute] Guid id, [FromBody] UpdateUserDeviceTokenRequest request) + { + try + { + var response = await _userDeviceTokenService.UpdateTokenAsync(id, request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(response, "Device token updated successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update device token: {TokenId}", id); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to update device token", ex.Message)); + } + } + + // [Obsolete("This method will be removed in v2.0. Please use GetFcmTokensByUserEmail instead.")] + // [HttpGet("user/{userId:guid}/tokens")] + // public async Task GetFcmTokensByUserId([FromRoute] Guid userId) + // { + // try + // { + // var tokens = await _userDeviceTokenService.GetFcmTokensByUserIdAsync(userId); + // return Ok(ApiResponseBuilder.BuildSuccessResponse(tokens)); + // } + // catch (Exception ex) + // { + // _logger.LogError(ex, "Failed to get FCM tokens for user: {UserId}", userId); + // return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve FCM tokens", ex.Message)); + // } + // } + + [HttpGet("user/{email}/tokens")] + public async Task GetFcmTokensByUserId([FromRoute] string email) + { + try + { + var tokens = await _userDeviceTokenService.GetFcmTokensByUserEmailAsync(email); + return Ok(ApiResponseBuilder.BuildSuccessResponse(tokens)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get FCM tokens for user: {email}", email); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve FCM tokens", ex.Message)); + } + } + + [HttpGet("my-tokens")] + public async Task GetMyFcmTokens() + { + try + { + var currentUser = GetCurrentUser(); + var tokens = await _userDeviceTokenService.GetFcmTokensByUserEmailAsync(currentUser.Email); + return Ok(ApiResponseBuilder.BuildSuccessResponse(tokens, "Your FCM tokens retrieved successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to get current user's FCM tokens"); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to retrieve your FCM tokens", ex.Message)); + } + } + + [HttpDelete("{id:guid}")] + public async Task DeleteToken([FromRoute] Guid id) + { + try + { + await _userDeviceTokenService.DeleteTokenAsync(id); + return Ok(ApiResponseBuilder.BuildSuccessResponse(null, "Device token deleted successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete device token: {TokenId}", id); + return BadRequest(ApiResponseBuilder.BuildErrorResponse("Failed to delete device token", ex.Message)); + } + } + + private CurrentUser GetCurrentUser() + { + // Extract user information from JWT token claims + var userIdClaim = HttpContext.User.FindFirst("sub")?.Value ?? + HttpContext.User.FindFirst("userId")?.Value ?? + HttpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + + var emailClaim = HttpContext.User.FindFirst(ClaimTypes.Email)?.Value ?? + HttpContext.User.FindFirst(JwtRegisteredClaimNames.UniqueName)?.Value ?? + HttpContext.User.FindFirst("email")?.Value; + + if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId)) + throw new UnauthorizedAccessException("User ID not found in token"); + + return new CurrentUser { Id = userId, Email = emailClaim }; + } + + private class CurrentUser + { + public Guid Id { get; set; } + public string Email { get; set; } + } + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/UserPlanController.cs b/TaskGenix.Server/Controllers/UserPlanController.cs new file mode 100644 index 0000000..0618357 --- /dev/null +++ b/TaskGenix.Server/Controllers/UserPlanController.cs @@ -0,0 +1,76 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.UserPlan; + +namespace TaskGenix.Server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class UserPlanController : BaseController + { + private readonly IUserPlanService _userPlanService; + + public UserPlanController(ILogger logger, IUserPlanService userPlanService) + : base(logger) + { + _userPlanService = userPlanService; + } + + [HttpGet("all")] + public async Task GetAllUserPlans() + { + var userPlans = await _userPlanService.GetAllUserPlans(); + return Ok(ApiResponseBuilder.BuildSuccessResponse(userPlans)); + } + + [HttpPost] + public async Task CreateUserPlan([FromBody] UserPlanRequest request) + { + var result = await _userPlanService.CreateUserPlan(request); + if (result == null) + return NotFound(); + return Ok(result); + } + + [HttpGet] + public async Task GetUserPlans([FromQuery] Guid? userId, [FromQuery] Guid? planId) + { + var userPlans = await _userPlanService.GetAllUserPlans(); + + if (userId.HasValue) + userPlans = userPlans.Where(up => up.UserId == userId.Value); + + if (planId.HasValue) + userPlans = userPlans.Where(up => up.PlanId == planId.Value); + + return Ok(userPlans); + } + + [HttpGet("{id}")] + public async Task GetUserPlanById(Guid id) + { + var userPlan = await _userPlanService.GetUserPlanById(id); + return Ok(userPlan); + } + + [HttpPut("{id}")] + public async Task UpdateUserPlan(Guid id, [FromBody] UpdateUserPlanRequest request) + { + var result = await _userPlanService.UpdateUserPlan(id, request); + if (result == null) + return NotFound(); + return Ok(result); + } + + [HttpDelete("{id}")] + public async Task DeleteUserPlan(Guid id) + { + var success = await _userPlanService.DeleteUserPlan(id); + if (!success) + return NotFound(); + return NoContent(); + } + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Controllers/UserPlanFeatureController.cs b/TaskGenix.Server/Controllers/UserPlanFeatureController.cs new file mode 100644 index 0000000..efd393b --- /dev/null +++ b/TaskGenix.Server/Controllers/UserPlanFeatureController.cs @@ -0,0 +1,102 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Meta; +using TaskGenix.Core.Requests.UserPlanFeature; + +namespace TaskGenix.Server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class UserPlanFeatureController : BaseController + { + private readonly IUserPlanFeatureService _service; + + public UserPlanFeatureController(ILogger logger, IUserPlanFeatureService service) + : base(logger) + { + _service = service; + } + + // POST: api/UserPlanFeature + [HttpPost] + public async Task Create([FromBody] CreateUserPlanFeatureRequest request) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + try + { + var result = await _service.CreateUserPlanFeature(request); + return Ok(ApiResponseBuilder.BuildSuccessResponse(result)); + } + catch (KeyNotFoundException knf) + { + _logger.LogWarning(knf, "Create user plan feature validation failed"); + return NotFound(new { message = knf.Message }); + } + catch (InvalidOperationException inv) + { + _logger.LogWarning(inv, "Create user plan feature conflict"); + return BadRequest(new { message = inv.Message }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating user plan feature"); + return StatusCode(500, new { message = "Internal server error" }); + } + } + + // PUT: api/UserPlanFeature + [HttpPut] + public async Task Update([FromBody] UpdateUserPlanFeatureRequest request) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + try + { + var result = await _service.UpdateUserPlanFeature(request); + if (!result.Updated) + { + return NotFound(ApiResponseBuilder.BuildErrorResponse(result.Message ?? "Update failed")); + } + + return Ok(ApiResponseBuilder.BuildSuccessResponse(result.Data)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error updating user plan feature"); + return StatusCode(500, new { message = "Internal server error" }); + } + } + // DELETE: api/UserPlanFeature/{id} + [HttpDelete("{id}")] + public async Task Delete(Guid id) + { + try + { + var result = await _service.DeleteUserPlanFeature(id); + if (!result) + { + return NotFound(ApiResponseBuilder.BuildErrorResponse("UserPlanFeature not found")); + } + + return Ok(ApiResponseBuilder.BuildSuccessResponse(new { success = true }, "UserPlanFeature deleted successfully")); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting user plan feature"); + return StatusCode(500, new { message = "Internal server error" }); + } + } + + // GET: api/UserPlanFeature/by-userplan/{userPlanId} + [HttpGet("by-userplan/{userPlanId}")] + public async Task GetByUserPlanId(Guid userPlanId) + { + var result = await _service.GetUserPlanFeaturesByUserPlanId(userPlanId); + return Ok(ApiResponseBuilder.BuildSuccessResponse(result)); + } + } +} diff --git a/TaskGenix.Server/DI/DependencyInjection.cs b/TaskGenix.Server/DI/DependencyInjection.cs new file mode 100644 index 0000000..301df07 --- /dev/null +++ b/TaskGenix.Server/DI/DependencyInjection.cs @@ -0,0 +1,223 @@ +using Amazon.S3; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Microsoft.SemanticKernel.ChatCompletion; +using System.Security.Claims; +using System.Text; +using TaskGenix.BLL.Services.Implements; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; + +namespace TaskGenix.Server.DI; + +public static class DependencyInjection +{ + #region service he thong + + public static IServiceCollection AddServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + + #endregion + + #region authen vs author + + public static IServiceCollection AddAuthenticateAuthor(this IServiceCollection services, + IConfiguration configuration) + { + services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = configuration["Jwt:Issuer"], + ValidAudience = configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(configuration["Jwt:SecretKey"])) + }; + options.Authority = "https://taskgenix.jp.auth0.com/"; + + options.Audience = "https://taskgenix.com/api"; + + options.RequireHttpsMetadata = true; + }); + + services.AddAuthorizationBuilder() + .AddPolicy("Admin", policy => policy.RequireClaim(ClaimTypes.Role, UserRole.Admin.ToString())) + .AddPolicy("User", policy => policy.RequireClaim(ClaimTypes.Role, UserRole.User.ToString())) + .AddPolicy("Guest", policy => policy.RequireClaim(ClaimTypes.Role, UserRole.Guest.ToString())); + return services; + } + + #endregion + + #region swagger + + public static IServiceCollection AddSwagger(this IServiceCollection services) + { + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "TaskGenix.API", + Version = "v1", + Description = "A TaskGenix Project" + }); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Description = + "JWT Authorization header using the Bearer scheme. Example: \"Bearer {token}\"", + Name = "Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer" + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] { } + } + }); + + c.EnableAnnotations(); + + c.AddServer(new OpenApiServer + { + Url = "http://localhost:5009", + Description = "Development" + }); + + c.AddServer(new OpenApiServer + { + Url = "http://localhost:8386", + Description = "Development Docker" + }); + + c.AddServer(new OpenApiServer + { + Url = "https://localhost:7026", + Description = "Local HTTPS" + }); + + c.AddServer(new OpenApiServer + { + Url = "https://taskgenix.com.vn", + Description = "Production Phake" + }); + + c.AddServer(new OpenApiServer + { + Url = "https://tgx.lch.id.vn", + Description = "Production Real" + }); + + c.AddServer(new OpenApiServer + { + Url = "https://api.taskgenix.website", + Description = "Production Real 2" + }); + }); + return services; + } + + #endregion + + public static IServiceCollection AddStorage(this IServiceCollection services, IConfiguration configuration) + { + var r2Config = configuration.GetSection("CloudflareR2"); + + var s3Config = new AmazonS3Config + { + ServiceURL = r2Config["ServiceUrl"], + AuthenticationRegion = "auto", // Use 'auto' for Cloudflare R2 global region + ForcePathStyle = true + }; + + // Đăng ký IAmazonS3 Client + services.AddSingleton(sp => + new AmazonS3Client( + r2Config["AccessKeyId"], + r2Config["SecretAccessKey"], + s3Config + ) + ); + + return services; + } + + public static IServiceCollection AddAIService(this IServiceCollection services, IConfiguration configuration) + { + var kernelBuilder = services.AddKernel(); + + var summarizeModel = configuration.GetSection("AI:Gemini:Summarize:Model").Get() + ?? throw new InvalidOperationException("AI:Gemini:Summarize:Model configuration is missing"); + var summarizeApiKey = configuration.GetSection("AI:Gemini:Summarize:APIKey").Get() + ?? throw new InvalidOperationException("AI:Gemini:Summarize:APIKey configuration is missing"); + var taskGenModel = configuration.GetSection("AI:Gemini:TaskGen:Model").Get() + ?? throw new InvalidOperationException("AI:Gemini:TaskGen:Model configuration is missing"); + var taskGenApiKey = configuration.GetSection("AI:Gemini:TaskGen:APIKey").Get() + ?? throw new InvalidOperationException("AI:Gemini:TaskGen:APIKey configuration is missing"); + + // Add logging to debug configuration values + Console.WriteLine($"Summarize Model: {summarizeModel}"); + Console.WriteLine($"TaskGen Model: {taskGenModel}"); + Console.WriteLine($"API Keys loaded: {summarizeApiKey.Length} and {taskGenApiKey.Length} characters"); + + // Register HttpClient for Gemini services + services.AddHttpClient(); + + // Register custom Gemini chat completion services + services.AddKeyedSingleton("gemini-summarize", (serviceProvider, serviceKey) => + new GeminiChatCompletionService( + summarizeModel, + summarizeApiKey, + serviceProvider.GetRequiredService().CreateClient(), + serviceProvider.GetRequiredService>())); + + services.AddKeyedSingleton("gemini-taskgen", (serviceProvider, serviceKey) => + new GeminiChatCompletionService( + taskGenModel, + taskGenApiKey, + serviceProvider.GetRequiredService().CreateClient(), + serviceProvider.GetRequiredService>())); + + return services; + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Hubs/ProjectProgressHub.cs b/TaskGenix.Server/Hubs/ProjectProgressHub.cs new file mode 100644 index 0000000..21d1df0 --- /dev/null +++ b/TaskGenix.Server/Hubs/ProjectProgressHub.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.SignalR; +using TaskGenix.BLL.Services.Interfaces; + +namespace TaskGenix.Server.Hubs; + +public class ProjectProgressHub : Hub +{ + private readonly IProjectProgressCalculator _progressCalculator; + private readonly ILogger _logger; + + public ProjectProgressHub( + IProjectProgressCalculator progressCalculator, + ILogger logger) + { + _progressCalculator = progressCalculator; + _logger = logger; + } + + public async Task JoinProjectGroup(string projectId) + { + await Groups.AddToGroupAsync(Context.ConnectionId, $"project_{projectId}"); + _logger.LogInformation("User {UserId} joined project {ProjectId} group", Context.UserIdentifier, projectId); + } + + public async Task LeaveProjectGroup(string projectId) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"project_{projectId}"); + _logger.LogInformation("User {UserId} left project {ProjectId} group", Context.UserIdentifier, projectId); + } + + public async Task RequestProgressUpdate(string projectId) + { + // Tính progress real-time khi user request + // Có thể cache kết quả để tối ưu + await Clients.Caller.SendAsync("ProgressUpdateRequested", new { ProjectId = projectId }); + } +} \ No newline at end of file diff --git a/TaskGenix.Server/Program.cs b/TaskGenix.Server/Program.cs index 175d904..f20e6f8 100644 --- a/TaskGenix.Server/Program.cs +++ b/TaskGenix.Server/Program.cs @@ -1,125 +1,189 @@ - +using FirebaseAdmin; +using Google.Apis.Auth.OAuth2; using Mapster; using MapsterMapper; +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using Microsoft.SemanticKernel; +using Scalar.AspNetCore; +using System.Reflection; +using System.Security.Claims; +using System.Text; using System.Text.Json.Serialization; using TaskGenix.BLL.Services.Implements; using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Domains; using TaskGenix.Core.Extensions; using TaskGenix.Core.Mappings; using TaskGenix.Core.Middlewares; +using TaskGenix.Core.Requests.Payment; +using TaskGenix.Core.Settings; using TaskGenix.DAL.Data; using TaskGenix.Repositories.Implements; using TaskGenix.Repositories.Interfaces; +using TaskGenix.Server.DI; + +namespace TaskGenix.Server; -namespace TaskGenix.Server +public class Program { - public class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - var builder = WebApplication.CreateBuilder(args); + var builder = WebApplication.CreateBuilder(args); - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(options => - { - options.SwaggerDoc("v1", new OpenApiInfo + builder.Services.AddDbContext(options => + { + options.UseNpgsql(builder.Configuration.GetConnectionString("SupabaseConnection"), + sqlOptions => { - Title = "TaskGenix.API", - Version = "v1", - Description = "A TaskGenix Project" + sqlOptions.EnableRetryOnFailure( + 5, + TimeSpan.FromSeconds(30), + null); }); - }); + }); - builder.Services.AddDbContext(options => - { - options.UseNpgsql(builder.Configuration.GetConnectionString("SupabaseConnection"), - npgsqlOptionsAction: sqlOptions => - { - sqlOptions.EnableRetryOnFailure( - maxRetryCount: 5, - maxRetryDelay: TimeSpan.FromSeconds(30), - errorCodesToAdd: null); - }); - }); + // Add IUnitOfWork and UnitOfWork + builder.Services.AddScoped, UnitOfWork>(); - // Add services to the container. + // Register Mapster + var config = TypeAdapterConfig.GlobalSettings; + MapsterConfig.RegisterMappings(); + builder.Services.AddSingleton(config); + builder.Services.AddScoped(); - // Add IUnitOfWork and UnitOfWork - builder.Services.AddScoped, UnitOfWork>(); - // Register Mapster - var config = TypeAdapterConfig.GlobalSettings; - MapsterConfig.RegisterMappings(); - builder.Services.AddSingleton(config); - builder.Services.AddScoped(); + builder.Services.Configure(builder.Configuration.GetSection("PayOS")); + builder.Services.Configure(builder.Configuration.GetSection("Email")); - // Add Service Layer - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); + // Add Service Scope in Here + builder.Services.AddServices(); + builder.Services.AddStorage(builder.Configuration); + builder.Services.AddMemoryCache(); + builder.Services.AddAIService(builder.Configuration); - builder.Services.AddCors(options => + builder.Services.AddCors(options => + { + options.AddPolicy("AllowAll", policy => { - options.AddPolicy("AllowAll", policy => - { - policy.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader(); - }); + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); }); + }); + + // Add this line before registering your services + builder.Services.AddHttpContextAccessor(); + + builder.Services.AddControllers().AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + + builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); - // Add this line before registering your services - builder.Services.AddHttpContextAccessor(); + // // Đăng ký JWT Authentication + builder.Services.AddAuthenticateAuthor(builder.Configuration); - builder.Services.AddControllers().AddJsonOptions(options => + // Add Swagger with JWT support + builder.Services.AddSwagger(); + + // Bind Kestrel to all network interfaces + builder.WebHost.ConfigureKestrel((context, options) => + { + if (context.HostingEnvironment.IsDevelopment()) { - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); + // Local dev: both HTTP and HTTPS + options.ListenAnyIP(5009); + // options.ListenLocalhost(5009); // HTTP + options.ListenLocalhost(7026, listenOptions => listenOptions.UseHttps()); + } + else + { + // Docker/Production: HTTP only + options.ListenAnyIP(5009); + } + }); + - builder.Services.AddControllers(); - // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(); + builder.Services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); - builder.Services.Configure(options => + // Firebase + try + { + // Use fixed path for Firebase credentials - works for both development and production + var credentialsPath = File.Exists("/app/firebase-service-account.json") + ? "/app/firebase-service-account.json" + : "firebase-service-account.json"; + + var firebaseApp = FirebaseApp.Create(new AppOptions { - options.SuppressModelStateInvalidFilter = true; + Credential = GoogleCredential.FromFile(credentialsPath) }); - var app = builder.Build(); + builder.Services.AddSingleton(firebaseApp); + } + catch (Exception ex) + { + throw new FileNotFoundException("Failed to initialize Firebase", ex); + } + builder.Services.Configure(options => + { + options.LowercaseUrls = true; // Forces lowercase routes + }); - app.UseCors("AllowAll"); + var app = builder.Build(); - app.UseApiExceptionHandler(); + app.UseCors("AllowAll"); + app.UseApiExceptionHandler(); - // Make sure Authentication is before Authorization - app.UseAuthentication(); - app.UseAuthorization(); + // Make sure Authentication is before Authorization + app.UseAuthentication(); + app.UseAuthorization(); - // Configure the HTTP request pipeline. - if (app.Environment.IsDevelopment()) - { - if (builder.Configuration.GetValue("ApplyMigrations", false)) - { - app.ApplyMigrations(); - } - app.UseSwagger(); - app.UseSwaggerUI(); - } + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + if (builder.Configuration.GetValue("ApplyMigrations", false)) + app.ApplyMigrations(); - app.UseHttpsRedirection(); + } - app.UseAuthorization(); + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseStaticFiles(); - app.MapControllers(); + var webRoot = app.Environment.WebRootPath; + // var customCss = File.ReadAllText(Path.Combine(webRoot, "assets", "ezyfix-scalar.css")); + // var faviconPath = Path.Combine(webRoot, "assets", "ezyfix.png"); - app.Run(); - } + app.MapScalarApiReference(options => + { + options.WithTitle("TaskGenix API Reference"); + // .WithCustomCss(customCss) + // .WithFavicon(faviconPath); + + options.WithOpenApiRoutePattern("/swagger/{documentName}/swagger.json"); + options.AddDocument( + documentName: "v1", + title: "TaskGenix API v1", + routePattern: "/swagger/v1/swagger.json", + isDefault: true); + }); + + // if (app.Environment.IsProduction()) app.UseHttpsRedirection(); + + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); } -} +} \ No newline at end of file diff --git a/TaskGenix.Server/TaskGenix.Server.csproj b/TaskGenix.Server/TaskGenix.Server.csproj index 37802ca..123fc76 100644 --- a/TaskGenix.Server/TaskGenix.Server.csproj +++ b/TaskGenix.Server/TaskGenix.Server.csproj @@ -1,28 +1,28 @@ - - net8.0 - enable - enable - 06584ca1-c25e-4af9-a2c9-ac106dd691de - Linux - + + net8.0 + enable + enable + 06584ca1-c25e-4af9-a2c9-ac106dd691de + Linux + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + - - - - - - - + + + diff --git a/TaskGenix.Server/TaskGenix.Server.http b/TaskGenix.Server/TaskGenix.Server.http index f226868..2e2ba5e 100644 --- a/TaskGenix.Server/TaskGenix.Server.http +++ b/TaskGenix.Server/TaskGenix.Server.http @@ -1,4 +1,4 @@ -@TaskGenix.Server_HostAddress = http://localhost:5009 +@TaskGenix.Server_HostAddress = http://localhost:8080 GET {{TaskGenix.Server_HostAddress}}/weatherforecast/ Accept: application/json diff --git a/TaskGenix.Tests.Integration/Common/InMemoryWebApplicationFactory.cs b/TaskGenix.Tests.Integration/Common/InMemoryWebApplicationFactory.cs new file mode 100644 index 0000000..1cb99fe --- /dev/null +++ b/TaskGenix.Tests.Integration/Common/InMemoryWebApplicationFactory.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Configuration; +using Moq; +using TaskGenix.DAL.Data; + +namespace TaskGenix.Tests.Integration.Common; + +public class InMemoryWebApplicationFactory + : WebApplicationFactory where TProgram : class +{ + private readonly string _databaseName; + + // Public parameterless constructor for test framework compatibility + public InMemoryWebApplicationFactory() : this(null) + { + } + + public InMemoryWebApplicationFactory(string? databaseName = null) + { + _databaseName = databaseName ?? Guid.NewGuid().ToString(); + } + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration((context, config) => + { + // Remove previous config sources and add test-specific appsettings + config.Sources.Clear(); + config.AddJsonFile("appsettings.Test.json", true); + }); + + builder.ConfigureServices(services => + { + // Remove the existing DbContext registration + services.RemoveAll(typeof(DbContextOptions)); + services.RemoveAll(typeof(TaskGenixDbContext)); + + // Add InMemory database + services.AddDbContext(options => + { + options.UseInMemoryDatabase(_databaseName) + .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning)); + }); + + // // --- Add Fake IHttpContextAccessor --- + // services.RemoveAll(); + // + // var httpContext = new DefaultHttpContext + // { + // Connection = { RemoteIpAddress = System.Net.IPAddress.Parse("127.0.0.1") } + // }; + // httpContext.Request.Headers["User-Agent"] = "IntegrationTest-Agent"; + // + // var httpContextAccessor = new Mock(); + // httpContextAccessor.Setup(x => x.HttpContext).Returns(httpContext); + // + // services.AddSingleton(httpContextAccessor.Object); + + // Ensure the database is created + var serviceProvider = services.BuildServiceProvider(); + using var scope = serviceProvider.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureCreated(); + }); + } + + public TaskGenixDbContext GetDbContext() + { + var scope = Services.CreateScope(); + return scope.ServiceProvider.GetRequiredService(); + } +} \ No newline at end of file diff --git a/TaskGenix.Tests.Integration/Common/IntegrationTestBase.cs b/TaskGenix.Tests.Integration/Common/IntegrationTestBase.cs new file mode 100644 index 0000000..5b813b3 --- /dev/null +++ b/TaskGenix.Tests.Integration/Common/IntegrationTestBase.cs @@ -0,0 +1,79 @@ +using System.Text; +using System.Text.Json; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using TaskGenix.Core.Meta; +using TaskGenix.Server; +using TaskGenix.Tests.Integration.Common; + +namespace TaskGenix.Tests.Integration.Common; + +public abstract class IntegrationTestBase : IDisposable where TFactory : WebApplicationFactory, new() +{ + protected readonly TFactory Factory; + protected readonly HttpClient Client; + protected readonly JsonSerializerOptions JsonOptions; + + protected IntegrationTestBase() + { + Factory = new TFactory(); + Client = Factory.CreateClient(); + JsonOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true + }; + } + + protected async Task SendAsync(HttpRequestMessage request) + { + var response = await Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + throw new InvalidOperationException($"Request failed with status {response.StatusCode}: {content}"); + + var result = JsonSerializer.Deserialize(content, JsonOptions); + result.Should().NotBeNull(); + return result!; + } + + protected async Task> SendSuccessAsync(HttpRequestMessage request) + { + return await SendAsync>(request); + } + + protected async Task SendErrorAsync(HttpRequestMessage request) + { + var response = await Client.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + response.IsSuccessStatusCode.Should().BeFalse(); + + var result = JsonSerializer.Deserialize(content, JsonOptions); + result.Should().NotBeNull(); + return result!; + } + + protected HttpRequestMessage CreatePostRequest(string endpoint, T data) + { + var json = JsonSerializer.Serialize(data, JsonOptions); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + return new HttpRequestMessage(HttpMethod.Post, endpoint) + { + Content = content + }; + } + + protected HttpRequestMessage CreateGetRequest(string endpoint) + { + return new HttpRequestMessage(HttpMethod.Get, endpoint); + } + + public void Dispose() + { + Client?.Dispose(); + Factory?.Dispose(); + } +} \ No newline at end of file diff --git a/TaskGenix.Tests.Integration/Controllers/AuthControllerIT.cs b/TaskGenix.Tests.Integration/Controllers/AuthControllerIT.cs new file mode 100644 index 0000000..6264e6f --- /dev/null +++ b/TaskGenix.Tests.Integration/Controllers/AuthControllerIT.cs @@ -0,0 +1,330 @@ +using FluentAssertions; +using TaskGenix.Core.Constants; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.Auth; +using TaskGenix.Core.Responses.Auth; +using TaskGenix.Server; +using TaskGenix.Tests.Integration.Common; +using Xunit; + +namespace TaskGenix.Tests.Integration.Controllers; + +// Ensures tests in this class share context and DB state if needed +[Collection("IntegrationTestCollection")] +public class AuthControllerIT : IntegrationTestBase> +{ + [Fact] + public async Task Register_WithValidData_ShouldCreateUser() + { + // Arrange + var request = new RegisterRequest + { + Username = "testuser", + Email = "test@example.com", + Password = "SecurePassword123!" + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/register", request); + var response = await SendSuccessAsync(httpRequest); + + // Assert + response.Message.Should().Be("Registration successful"); + + // Verify user was created in database + using var context = Factory.GetDbContext(); + var user = context.Users.FirstOrDefault(u => u.Email == request.Email); + user.Should().NotBeNull(); + user!.Username.Should().Be(request.Username); + user.IsActive.Should().BeTrue(); + } + + [Fact] + public async Task Register_WithExistingEmail_ShouldReturnError() + { + // Arrange + var existingUser = new User + { + Id = Guid.NewGuid(), + Username = "existing", + Email = "existing@example.com", + PasswordHash = "$2a$11$abcdefghijklmnopqrstuvwxyz", + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + using (var context = Factory.GetDbContext()) + { + context.Users.Add(existingUser); + await context.SaveChangesAsync(); + } + + var request = new RegisterRequest + { + Username = "newuser", + Email = "existing@example.com", // Same email + Password = "SecurePassword123!" + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/register", request); + var errorResponse = await SendErrorAsync(httpRequest); + + // Assert + errorResponse.Message.Should().Be("Bad Request - The request was invalid or cannot be served"); + errorResponse.Reason.Should().Contain("already in use"); + } + + [Fact] + public async Task Login_WithValidCredentials_ShouldReturnToken() + { + // Arrange + var password = "SecurePassword123!"; + var user = new User + { + Id = Guid.NewGuid(), + Username = "testuser", + Email = "test@example.com", + PasswordHash = BCrypt.Net.BCrypt.HashPassword(password), + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + using (var context = Factory.GetDbContext()) + { + context.Users.Add(user); + await context.SaveChangesAsync(); + } + + var request = new LoginRequest + { + Email = user.Email, + Password = password + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/login", request); + var response = await SendSuccessAsync(httpRequest); + + // Assert + response.Message.Should().Be("Login successful"); + response.Data.Should().NotBeNull(); + response.Data!.Token.Should().NotBeNull(); + response.Data.Token.RefreshToken.Should().NotBeNullOrEmpty(); + response.Data.Account.Email.Should().Be(user.Email); + response.Data.Account.Username.Should().Be(user.Username); + } + + [Fact] + public async Task Login_WithInvalidCredentials_ShouldReturnError() + { + // Arrange + var request = new LoginRequest + { + Email = "nonexistent@example.com", + Password = "wrongpassword" + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/login", request); + var errorResponse = await SendErrorAsync(httpRequest); + + // Assert + errorResponse.Message.Should().Be("Bad Request - The request was invalid or cannot be served"); + errorResponse.Reason.Should().Contain("Invalid credentials"); + } + + [Fact] + public async Task MobileLoginRequest_WithValidEmail_ShouldSendOtp() + { + // Arrange + var user = new User + { + Id = Guid.NewGuid(), + Username = "testuser", + Email = "test@example.com", + PasswordHash = "$2a$11$abcdefghijklmnopqrstuvwxyz", + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + using (var context = Factory.GetDbContext()) + { + context.Users.Add(user); + await context.SaveChangesAsync(); + } + + var request = new MobileLoginRequest + { + Email = user.Email + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/mobile/login-request", request); + var response = await SendSuccessAsync(httpRequest); + + // Assert + response.Message.Should().Be("OTP sent successfully"); + } + + [Fact] + public async Task MobileLoginRequest_WithNonExistentEmail_ShouldReturnError() + { + // Arrange + var request = new MobileLoginRequest + { + Email = "nonexistent@example.com" + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/mobile/login-request", request); + var errorResponse = await SendErrorAsync(httpRequest); + + // Assert + errorResponse.Message.Should().Be("Bad Request - The request was invalid or cannot be served"); + errorResponse.Reason.Should().Contain("User not found"); + } + + [Fact] + public async Task VerifyOtp_WithValidOtp_ShouldReturnToken() + { + // Arrange + var user = new User + { + Id = Guid.NewGuid(), + Username = "testuser", + Email = "test@example.com", + PasswordHash = "$2a$11$abcdefghijklmnopqrstuvwxyz", + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + using (var context = Factory.GetDbContext()) + { + context.Users.Add(user); + await context.SaveChangesAsync(); + } + + // First request OTP + var loginRequest = new MobileLoginRequest { Email = user.Email }; + var httpLoginRequest = CreatePostRequest("/api/auth/mobile/login-request", loginRequest); + await SendSuccessAsync(httpLoginRequest); + + // For testing purposes, we'll use a known OTP or mock the verification + var verifyRequest = new VerifyOtpRequest + { + Email = user.Email, + Code = "123456" // In real scenario, this would be generated and stored + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/mobile/verify-otp", verifyRequest); + + // Note: This test might fail because OTP verification depends on the actual implementation + // In a real scenario, you would need to either: + // 1. Mock the OTP service + // 2. Use a test OTP that's always valid + // 3. Retrieve the actual OTP from the database/cache + + try + { + var response = await SendSuccessAsync(httpRequest); + + // Assert + response.Message.Should().Be("OTP verified successfully"); + response.Data.Should().NotBeNull(); + response.Data!.Token.Should().NotBeNull(); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("Invalid or expired OTP")) + { + // This is expected if OTP validation is strict + // In a real test environment, you'd configure test OTPs + Assert.True(true, "OTP validation is working as expected"); + } + } + + [Fact] + public async Task ResendOtp_WithValidEmail_ShouldReturnSuccess() + { + // Arrange + var user = new User + { + Id = Guid.NewGuid(), + Username = "testuser", + Email = "test@example.com", + PasswordHash = "$2a$11$abcdefghijklmnopqrstuvwxyz", + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + using (var context = Factory.GetDbContext()) + { + context.Users.Add(user); + await context.SaveChangesAsync(); + } + + var request = new MobileLoginRequest + { + Email = user.Email + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/mobile/resend-otp", request); + var response = await SendSuccessAsync(httpRequest); + + // Assert + response.Message.Should().Be("OTP resent successfully"); + response.Data.Should().NotBeNull(); + response.Data!.Email.Should().Be(user.Email); + response.Data.RemainingAttempts.Should().BeGreaterThan(0); + } + + [Fact] + public async Task RefreshToken_WithValidToken_ShouldReturnNewToken() + { + // Arrange + // First, login to get a refresh token + var password = "SecurePassword123!"; + var user = new User + { + Id = Guid.NewGuid(), + Username = "testuser", + Email = "test@example.com", + PasswordHash = BCrypt.Net.BCrypt.HashPassword(password), + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }; + + using (var context = Factory.GetDbContext()) + { + context.Users.Add(user); + await context.SaveChangesAsync(); + } + + var loginRequest = new LoginRequest { Email = user.Email, Password = password }; + var httpLoginRequest = CreatePostRequest("/api/auth/login", loginRequest); + var loginResponse = await SendSuccessAsync(httpLoginRequest); + + var refreshRequest = new RefreshTokenRequest + { + RefreshToken = loginResponse.Data!.Token.RefreshToken + }; + + // Act + var httpRequest = CreatePostRequest("/api/auth/refresh-token", refreshRequest); + var response = await SendSuccessAsync(httpRequest); + + // Assert + response.Message.Should().Be(CommonMessage.OperationSuccessful); + response.Data.Should().NotBeNull(); + response.Data!.Token.Should().NotBeNull(); + response.Data.Token.RefreshToken.Should().NotBeNullOrEmpty(); + response.Data.Token.Should().NotBe(loginResponse.Data.Token); // Should be a new token + } +} \ No newline at end of file diff --git a/TaskGenix.Tests.Integration/Controllers/IntegrationTestCollection.cs b/TaskGenix.Tests.Integration/Controllers/IntegrationTestCollection.cs new file mode 100644 index 0000000..528d868 --- /dev/null +++ b/TaskGenix.Tests.Integration/Controllers/IntegrationTestCollection.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace TaskGenix.Tests.Integration.Controllers; + +// Marker for shared context between integration tests +[CollectionDefinition("IntegrationTestCollection")] +public class IntegrationTestCollection : ICollectionFixture +{ +} \ No newline at end of file diff --git a/TaskGenix.Tests.Integration/Controllers/IntegrationTestFixture.cs b/TaskGenix.Tests.Integration/Controllers/IntegrationTestFixture.cs new file mode 100644 index 0000000..fca94d5 --- /dev/null +++ b/TaskGenix.Tests.Integration/Controllers/IntegrationTestFixture.cs @@ -0,0 +1,7 @@ +namespace TaskGenix.Tests.Integration.Controllers; + +// Shared fixture for integration tests, can be expanded for DB setup/teardown +public class IntegrationTestFixture +{ + // Add shared setup/teardown logic here if needed +} \ No newline at end of file diff --git a/TaskGenix.Tests.Integration/Controllers/UserControllerIT.cs b/TaskGenix.Tests.Integration/Controllers/UserControllerIT.cs new file mode 100644 index 0000000..952959f --- /dev/null +++ b/TaskGenix.Tests.Integration/Controllers/UserControllerIT.cs @@ -0,0 +1,30 @@ +// using FluentAssertions; +// using TaskGenix.Core.Constants; +// using TaskGenix.Core.Domains; +// using TaskGenix.Core.Requests.Auth; +// using TaskGenix.Core.Responses.Auth; +// using TaskGenix.Server; +// using TaskGenix.Tests.Integration.Common; +// using Xunit; +// +// namespace TaskGenix.Tests.Integration.Controllers +// { +// // Ensures tests in this class share context and DB state if needed +// [Collection("IntegrationTestCollection")] +// public class UserControllerIT : IntegrationTestBase> +// { +// [Fact] +// public async Task GetUserById_NonExistingId_ThrowsNotFoundException() +// { +// // Arrange +// var userId = Guid.NewGuid(); +// _mockUserService.Setup(s => s.GetUserById(It.IsAny())) +// .ThrowsAsync(new NotFoundException("User not found")); +// +// // Act & Assert +// var ex = await Assert.ThrowsAsync(() => _controller.GetUserById(userId)); +// Assert.Equal("User not found", ex.Message); +// } +// } +// } + diff --git a/TaskGenix.Tests.Integration/TaskGenix.Tests.Integration.csproj b/TaskGenix.Tests.Integration/TaskGenix.Tests.Integration.csproj new file mode 100644 index 0000000..4604e0a --- /dev/null +++ b/TaskGenix.Tests.Integration/TaskGenix.Tests.Integration.csproj @@ -0,0 +1,44 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + diff --git a/TaskGenix.Tests.Integration/TestFramework.cs b/TaskGenix.Tests.Integration/TestFramework.cs new file mode 100644 index 0000000..1b74d32 --- /dev/null +++ b/TaskGenix.Tests.Integration/TestFramework.cs @@ -0,0 +1,14 @@ +using Xunit.Abstractions; +using Xunit.Sdk; + +[assembly: TestFramework("TaskGenix.Tests.Integration.TestFramework", "TaskGenix.Tests.Integration")] + +namespace TaskGenix.Tests.Integration; + +public class TestFramework : XunitTestFramework +{ + public TestFramework(IMessageSink messageSink) + : base(messageSink) + { + } +} \ No newline at end of file diff --git a/TaskGenix.Tests.Integration/appsettings.Test.json b/TaskGenix.Tests.Integration/appsettings.Test.json new file mode 100644 index 0000000..37077db --- /dev/null +++ b/TaskGenix.Tests.Integration/appsettings.Test.json @@ -0,0 +1,15 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "DataSource=:memory:" + }, + "Jwt": { + "Key": "TestSecretKeyForJwt", + "Issuer": "TestIssuer", + "Audience": "TestAudience" + }, + "Logging": { + "LogLevel": { + "Default": "Warning" + } + } +} diff --git a/TaskGenix.Tests.Unit/Common/AutoMoqDataAttribute.cs b/TaskGenix.Tests.Unit/Common/AutoMoqDataAttribute.cs new file mode 100644 index 0000000..50af3d6 --- /dev/null +++ b/TaskGenix.Tests.Unit/Common/AutoMoqDataAttribute.cs @@ -0,0 +1,20 @@ +using AutoFixture; +using AutoFixture.Xunit2; + +namespace TaskGenix.Tests.Unit.Common; + +public class AutoMoqDataAttribute() : AutoDataAttribute(() => +{ + var fixture = new Fixture(); + fixture.Behaviors.OfType().ToList() + .ForEach(b => fixture.Behaviors.Remove(b)); + fixture.Behaviors.Add(new OmitOnRecursionBehavior()); + return fixture; +}); + +public class InlineAutoMoqDataAttribute : InlineAutoDataAttribute +{ + public InlineAutoMoqDataAttribute(params object[] values) : base(new AutoMoqDataAttribute(), values) + { + } +} \ No newline at end of file diff --git a/TaskGenix.Tests.Unit/Common/TestDataBuilder.cs b/TaskGenix.Tests.Unit/Common/TestDataBuilder.cs new file mode 100644 index 0000000..620fa20 --- /dev/null +++ b/TaskGenix.Tests.Unit/Common/TestDataBuilder.cs @@ -0,0 +1,133 @@ +using TaskGenix.Core.Domains; +using TaskGenix.Core.Requests.Auth; +using TaskGenix.Core.Responses.Auth; + +namespace TaskGenix.Tests.Unit.Common; + +public static class TestDataBuilder +{ + public static User CreateTestUser( + string email = "test@example.com", + string username = "testuser", + bool isActive = true) + { + return new User + { + Id = Guid.NewGuid(), + Email = email, + Username = username, + PasswordHash = "$2a$11$abcdefghijklmnopqrstuvwxyz", // Fake bcrypt hash + IsActive = isActive, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + LastLogin = null + }; + } + + public static User CreateTestUserWithFixedId( + string id, + string email = "test@example.com", + string username = "testuser", + bool isActive = true) + { + return new User + { + Id = Guid.Parse(id), + Email = email, + Username = username, + PasswordHash = "$2a$11$abcdefghijklmnopqrstuvwxyz", // Fake bcrypt hash + IsActive = isActive, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow, + LastLogin = DateTime.UtcNow + }; + } + + public static LoginRequest CreateLoginRequest( + string email = "test@example.com", + string password = "password123") + { + return new LoginRequest + { + Email = email, + Password = password + }; + } + + public static RegisterRequest CreateRegisterRequest( + string username = "testuser", + string email = "test@example.com", + string password = "password123") + { + return new RegisterRequest + { + Username = username, + Email = email, + Password = password + }; + } + + public static MobileLoginRequest CreateMobileLoginRequest( + string email = "test@example.com") + { + return new MobileLoginRequest + { + Email = email + }; + } + + public static VerifyOtpRequest CreateVerifyOtpRequest( + string email = "test@example.com", + string otp = "123456") + { + return new VerifyOtpRequest + { + Email = email, + Code = otp + }; + } + + public static RefreshTokenRequest CreateRefreshTokenRequest( + string refreshToken = "test-refresh-token") + { + return new RefreshTokenRequest + { + RefreshToken = refreshToken + }; + } + + public static LoginResponse CreateLoginResponse(User user) + { + return new LoginResponse + { + Token = new TokenResponse + { + AccessToken = "fake-jwt-token", + RefreshToken = "fake-refresh-token", + ExpirationDate = DateTime.UtcNow.AddHours(24).ToString(), + RefreshExpirationDate = DateTime.UtcNow.AddDays(182).ToString() + }, + Account = new UserInfo + { + Id = user.Id, + Username = user.Username, + Email = user.Email, + Role = "User" + } + }; + } + + public static ResendOtpResponse CreateResendOtpResponse( + string email = "test@example.com", + int remainingAttempts = 3, + int expiresInMinutes = 5) + { + return new ResendOtpResponse + { + Message = "OTP resent successfully", + Email = email, + RemainingAttempts = remainingAttempts, + ExpiresInMinutes = expiresInMinutes + }; + } +} \ No newline at end of file diff --git a/TaskGenix.Tests.Unit/Common/TestDbContextFactory.cs b/TaskGenix.Tests.Unit/Common/TestDbContextFactory.cs new file mode 100644 index 0000000..2418073 --- /dev/null +++ b/TaskGenix.Tests.Unit/Common/TestDbContextFactory.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using TaskGenix.DAL.Data; + +namespace TaskGenix.Tests.Unit.Common; + +public static class TestDbContextFactory +{ + public static TaskGenixDbContext CreateInMemoryDbContext(string dbName = "") + { + if (string.IsNullOrEmpty(dbName)) dbName = Guid.NewGuid().ToString(); + + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(dbName) + .Options; + + return new TaskGenixDbContext(options); + } +} \ No newline at end of file diff --git a/TaskGenix.Tests.Unit/Common/TestValidationHelper.cs b/TaskGenix.Tests.Unit/Common/TestValidationHelper.cs new file mode 100644 index 0000000..f68db9f --- /dev/null +++ b/TaskGenix.Tests.Unit/Common/TestValidationHelper.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc; + +namespace TaskGenix.Tests.Unit.Common; + +public static class TestValidationHelper +{ + public static void AddValidationErrorsToModelState(ControllerBase controller, object model) + { + var validationContext = new ValidationContext(model); + var validationResults = new List(); + Validator.TryValidateObject(model, validationContext, validationResults, true); + foreach (var validationResult in validationResults) + foreach (var memberName in validationResult.MemberNames) + controller.ModelState.AddModelError(memberName, validationResult.ErrorMessage ?? "Validation error"); + } +} \ No newline at end of file diff --git a/TaskGenix.Tests.Unit/Controllers/AuthControllerTests.cs b/TaskGenix.Tests.Unit/Controllers/AuthControllerTests.cs new file mode 100644 index 0000000..be92113 --- /dev/null +++ b/TaskGenix.Tests.Unit/Controllers/AuthControllerTests.cs @@ -0,0 +1,447 @@ +using TaskGenix.Core.Constants; +using TaskGenix.Core.Exceptions; + +namespace TaskGenix.Tests.Unit.Controllers; + +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using TaskGenix.BLL.Services.Interfaces; +using Core.Meta; +using Core.Requests.Auth; +using Core.Responses.Auth; +using TaskGenix.Server.Controllers; +using Common; +using Xunit; +using System.ComponentModel.DataAnnotations; + +public class AuthControllerTests : IDisposable +{ + private readonly Mock> _mockLogger; + private readonly Mock _mockAuthService; + private readonly AuthController _controller; + private bool _disposed; + + public AuthControllerTests() + { + _mockLogger = new Mock>(); + _mockAuthService = new Mock(); + _controller = new AuthController(_mockLogger.Object, _mockAuthService.Object); + _disposed = false; + } + + public void Dispose() + { + if (!_disposed) + { + // Clean up resources if needed + _mockLogger?.Reset(); + _mockAuthService?.Reset(); + // If you add disposable resources, dispose them here + _disposed = true; + } + + GC.SuppressFinalize(this); + } + + #region Login Tests + + [Fact] + public async Task Login_WithValidCredentials_ReturnsOkResult() + { + // Arrange + var request = TestDataBuilder.CreateLoginRequest(); + var user = TestDataBuilder.CreateTestUser(); + var expectedResponse = TestDataBuilder.CreateLoginResponse(user); + + _mockAuthService.Setup(x => x.LoginAsync(It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.Login(request); + + // Assert + result.Should().BeOfType(); + var okResult = (OkObjectResult)result; + okResult.Value.Should().BeOfType>(); + + var response = (SuccessResponse)okResult.Value!; + response.Message.Should().Be("Login successful"); + response.Data.Should().BeEquivalentTo(expectedResponse); + + _mockAuthService.Verify(x => x.LoginAsync(request), Times.Once); + } + + [Theory] + [AutoMoqData] + public async Task Login_WithAutoFixture_GeneratesRequest(LoginRequest request) + { + // Arrange + var expectedResponse = TestDataBuilder.CreateLoginResponse(TestDataBuilder.CreateTestUser()); + _mockAuthService.Setup(x => x.LoginAsync(It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.Login(request); + + // Assert + result.Should().BeOfType(); + var okResult = (OkObjectResult)result; + okResult.Value.Should().BeOfType>(); + + var response = (SuccessResponse)okResult.Value!; + response.Message.Should().Be("Login successful"); + response.Data.Should().BeEquivalentTo(expectedResponse); + + _mockAuthService.Verify(x => x.LoginAsync(request), Times.Once); + } + + [Fact] + public async Task Login_WithInvalidCredentials_ReturnsBadRequest() + { + // Arrange + var request = TestDataBuilder.CreateLoginRequest(); + _mockAuthService.Setup(x => x.LoginAsync(It.IsAny())) + .ThrowsAsync(new UnauthorizedException(CommonExceptionMessage.InvalidCredentials)); + + // Act + var ex = await Assert.ThrowsAsync(() => _controller.Login(request)); + + // Assert + ex.Message.Should().Be(CommonExceptionMessage.InvalidCredentials); + } + + [Theory] + [InlineData("")] + [InlineData("invalid-email")] + public async Task Login_WithInvalidEmail_ReturnsBadRequest(string email) + { + // Arrange + var request = TestDataBuilder.CreateLoginRequest(email); + + // Validate DTO and add errors to ModelState using shared helper + TestValidationHelper.AddValidationErrorsToModelState(_controller, request); + + // Act + var result = await _controller.Login(request); + + // Assert + result.Should().BeOfType(); + } + + #endregion + + #region Register Tests + + [Fact] + public async Task Register_WithValidData_ReturnsOkResult() + { + // Arrange + var request = TestDataBuilder.CreateRegisterRequest(); + + _mockAuthService.Setup(x => x.RegisterAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _controller.Register(request); + + // Assert + result.Should().BeOfType(); + var okResult = (OkObjectResult)result; + okResult.Value.Should().BeOfType>(); + + var response = (SuccessResponse)okResult.Value!; + response.Message.Should().Be(CommonMessage.OperationSuccessful); + + _mockAuthService.Verify(x => x.RegisterAsync(request), Times.Once); + } + + [Fact] + public async Task Register_WithInvalidData_ReturnsBadRequest() + { + // Arrange + var request = TestDataBuilder.CreateRegisterRequest( + email: "invalid-email", + username: + "verylongusernamethatexceedsthelimitverylongusernamethatexceedsthelimitverylongusernamethatexceedsthelimit" + ); + + // Validate DTO and add errors to ModelState using shared helper + TestValidationHelper.AddValidationErrorsToModelState(_controller, request); + + // Act + var result = await _controller.Register(request); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task Register_WithExistingEmail_ThrowsConflictException() + { + // Arrange + var request = TestDataBuilder.CreateRegisterRequest(); + + _mockAuthService.Setup(x => x.RegisterAsync(It.IsAny())) + .ThrowsAsync(new ConflictException(CommonExceptionMessage.EmailAlreadyInUse)); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _controller.Register(request)); + + ex.Message.Should().Be(CommonExceptionMessage.EmailAlreadyInUse); + } + + #endregion + + #region RefreshToken Tests + + [Fact] + public async Task RefreshToken_WithValidToken_ReturnsOkResult() + { + // Arrange + var request = TestDataBuilder.CreateRefreshTokenRequest(); + var user = TestDataBuilder.CreateTestUser(); + var expectedResponse = TestDataBuilder.CreateLoginResponse(user); + + _mockAuthService.Setup(x => x.RefreshTokenAsync(It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.RefreshToken(request); + + // Assert + result.Should().BeOfType(); + var okResult = (OkObjectResult)result; + okResult.Value.Should().BeOfType>(); + + var response = (SuccessResponse)okResult.Value!; + response.Message.Should().Be(CommonMessage.OperationSuccessful); + response.Data.Should().BeEquivalentTo(expectedResponse); + + _mockAuthService.Verify(x => x.RefreshTokenAsync(request.RefreshToken), Times.Once); + } + + [Fact] + public async Task RefreshToken_WithInvalidToken_ReturnsBadRequest() + { + // Arrange + var request = TestDataBuilder.CreateRefreshTokenRequest(); + _mockAuthService.Setup(x => x.RefreshTokenAsync(It.IsAny())) + .ThrowsAsync( + new UnauthorizedAccessException("Unauthorized - Authentication credentials are missing or invalid")); + + // Act + Assert + var ex = await Assert.ThrowsAsync(() => _controller.RefreshToken(request)); + ex.Message.Should().Be("Unauthorized - Authentication credentials are missing or invalid"); + } + + #endregion + + #region MobileLoginRequest Tests + + [Fact] + public async Task MobileLoginRequest_WithValidEmail_ReturnsOkResult() + { + // Arrange + var request = TestDataBuilder.CreateMobileLoginRequest(); + + _mockAuthService.Setup(x => x.RequestOtpAsync(It.IsAny())) + .Returns(Task.CompletedTask); + + // Act + var result = await _controller.MobileLoginRequest(request); + + // Assert + result.Should().BeOfType(); + var okResult = (OkObjectResult)result; + okResult.Value.Should().BeOfType>(); + + var response = (SuccessResponse)okResult.Value!; + response.Message.Should().Be(CommonMessage.OperationSuccessful); + + _mockAuthService.Verify(x => x.RequestOtpAsync(request.Email), Times.Once); + } + + [Fact] + public async Task MobileLoginRequest_WithInvalidEmail_ReturnsBadRequest() + { + // Arrange + var request = TestDataBuilder.CreateMobileLoginRequest("invalid-email"); + + // Validate DTO and add errors to ModelState using shared helper + TestValidationHelper.AddValidationErrorsToModelState(_controller, request); + + // Act + var result = await _controller.MobileLoginRequest(request); + + // Assert + result.Should().BeOfType(); + } + + #endregion + + #region VerifyOtp Tests + + [Fact] + public async Task VerifyOtp_WithValidOtp_ReturnsOkResult() + { + // Arrange + var request = TestDataBuilder.CreateVerifyOtpRequest(); + var user = TestDataBuilder.CreateTestUser(); + var expectedResponse = TestDataBuilder.CreateLoginResponse(user); + + _mockAuthService.Setup(x => x.VerifyOtpAsync(It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.VerifyOtp(request); + + // Assert + result.Should().BeOfType(); + var okResult = (OkObjectResult)result; + okResult.Value.Should().BeOfType>(); + + var response = (SuccessResponse)okResult.Value!; + response.Message.Should().Be(CommonMessage.OperationSuccessful); + response.Data.Should().BeEquivalentTo(expectedResponse); + + _mockAuthService.Verify(x => x.VerifyOtpAsync(request), Times.Once); + } + + [Fact] + public async Task VerifyOtp_WithInvalidData_ReturnsBadRequest() + { + // Arrange + var request = TestDataBuilder.CreateVerifyOtpRequest(otp: "123"); // Invalid OTP (too short) + + // Validate DTO and add errors to ModelState using shared helper + TestValidationHelper.AddValidationErrorsToModelState(_controller, request); + + // Act + var result = await _controller.VerifyOtp(request); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task VerifyOtp_WithInvalidOtp_ThrowsUnauthorizedException() + { + // Arrange + var request = TestDataBuilder.CreateVerifyOtpRequest(); + var exceptionMessage = "Invalid or expired OTP"; + + _mockAuthService.Setup(x => x.VerifyOtpAsync(It.IsAny())) + .ThrowsAsync(new UnauthorizedException(exceptionMessage)); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _controller.VerifyOtp(request)); + ex.Message.Should().Be(exceptionMessage); + } + + #endregion + + #region ResendOtp Tests + + [Fact] + public async Task ResendOtp_WithValidEmail_ReturnsOkResult() + { + // Arrange + var request = TestDataBuilder.CreateMobileLoginRequest(); + var expectedResponse = TestDataBuilder.CreateResendOtpResponse(); + + _mockAuthService.Setup(x => x.ResendOtpAsync(It.IsAny())) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.ResendOtp(request); + + // Assert + result.Should().BeOfType(); + var okResult = (OkObjectResult)result; + + okResult.Value.Should().BeOfType>(); + + var response = (SuccessResponse)okResult.Value!; + response.Message.Should().Be("OTP resent successfully"); + response.Data.Should().BeEquivalentTo(expectedResponse); + + _mockAuthService.Verify(x => x.ResendOtpAsync(request.Email), Times.Once); + } + + + [Fact] + public async Task ResendOtp_WithInvalidEmail_ReturnsBadRequest() + { + // Arrange + var request = TestDataBuilder.CreateMobileLoginRequest("invalid-email"); + + // Validate DTO and add errors to ModelState using shared helper + TestValidationHelper.AddValidationErrorsToModelState(_controller, request); + + // Act + var result = await _controller.ResendOtp(request); + + // Assert + result.Should().BeOfType(); + } + + [Fact] + public async Task ResendOtp_WithRateLimitExceeded_ReturnsBadRequest() + { + // Arrange + var request = TestDataBuilder.CreateMobileLoginRequest(); + var exceptionMessage = "Rate limit exceeded. Please wait before requesting another OTP."; + + _mockAuthService.Setup(x => x.ResendOtpAsync(It.IsAny())) + .ThrowsAsync(new UnauthorizedException(exceptionMessage)); + + // Act & Assert + var ex = await Assert.ThrowsAsync(() => _controller.ResendOtp(request)); + ex.Message.Should().Be(exceptionMessage); + } + + #endregion + + #region Logout Tests + + [Fact] + public async Task Logout_WithValidUser_ReturnsNoContent() + { + // Arrange + _mockAuthService.Setup(x => x.LogoutAsync()) + .Returns(Task.CompletedTask); + + // Act + var result = await _controller.Logout(); + + // Assert + result.Should().BeOfType(); + _mockAuthService.Verify(x => x.LogoutAsync(), Times.Once); + } + + // [Fact] + // public async Task Logout_WithServiceException_ReturnsBadRequest() + // { + // // Arrange + // var exceptionMessage = "Logout failed"; + // + // _mockAuthService.Setup(x => x.LogoutAsync()) + // .ThrowsAsync(new InvalidOperationException(exceptionMessage)); + // + // // Act + // var result = await _controller.Logout(); + // + // // Assert + // result.Should().BeOfType(); + // var badRequestResult = (BadRequestObjectResult)result; + // badRequestResult.Value.Should().BeOfType(); + // + // var errorResponse = (ErrorResponse)badRequestResult.Value!; + // errorResponse.Message.Should().Be("Logout failed"); + // errorResponse.Reason.Should().Be(exceptionMessage); + // } + + #endregion +} \ No newline at end of file diff --git a/TaskGenix.Tests.Unit/Controllers/TaskAssignmentControllerTest.cs b/TaskGenix.Tests.Unit/Controllers/TaskAssignmentControllerTest.cs new file mode 100644 index 0000000..eb67f7b --- /dev/null +++ b/TaskGenix.Tests.Unit/Controllers/TaskAssignmentControllerTest.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using TaskGenix.BLL.Services.Interfaces; +using TaskGenix.Core.Exceptions; +using TaskGenix.Core.Requests.TaskAssignment; +using TaskGenix.Core.Responses.TaskGen; +using TaskGenix.Server.Controllers; +using Xunit; + +namespace TaskGenix.Tests.Unit.Controllers; + +public class TaskAssignmentControllerTest : IDisposable +{ + private readonly Mock> _mockLogger; + private readonly Mock _mockTaskAssignmentService; + private readonly TaskAssignmentController _controller; + private bool _disposed; + + public TaskAssignmentControllerTest() + { + _mockLogger = new Mock>(); + _mockTaskAssignmentService = new Mock(); + + _controller = new TaskAssignmentController( + _mockLogger.Object, + _mockTaskAssignmentService.Object); + } + + public void Dispose() + { + if (!_disposed) + { + _mockLogger?.Reset(); + _mockTaskAssignmentService?.Reset(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } + + #region AssignUserToTask Tests + + [Fact] + public async Task AssignUserToTask_ValidRequest_ReturnsOkResult() + { + // Arrange + var request = new AssignUserToTaskRequest + { + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + var expectedResponse = new TaskAssignmentResponse + { + Id = Guid.NewGuid(), + TaskId = request.TaskId, + UserId = request.UserId, + AssignedAt = DateTime.UtcNow + }; + + _mockTaskAssignmentService.Setup(s => s.AssignUserToTaskAsync(request)) + .ReturnsAsync(expectedResponse); + + // Act + var result = await _controller.AssignUserToTask(request); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.AssignUserToTaskAsync(request), Times.Once); + } + + [Fact] + public async Task AssignUserToTask_ServiceThrowsException_ReturnsBadRequest() + { + // Arrange + var request = new AssignUserToTaskRequest + { + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + _mockTaskAssignmentService.Setup(s => s.AssignUserToTaskAsync(request)) + .ThrowsAsync(new NotFoundException("Task not found")); + + // Act + var result = await _controller.AssignUserToTask(request); + + // Assert + Assert.IsType(result); + } + + #endregion + + #region BulkAssignUsersToTask Tests + + [Fact] + public async Task BulkAssignUsersToTask_ValidRequest_ReturnsOkResult() + { + // Arrange + var request = new BulkAssignUsersRequest + { + TaskId = Guid.NewGuid(), + UserIds = new List { Guid.NewGuid(), Guid.NewGuid() } + }; + + var expectedResponses = new List + { + new() { Id = Guid.NewGuid(), TaskId = request.TaskId, UserId = request.UserIds[0] }, + new() { Id = Guid.NewGuid(), TaskId = request.TaskId, UserId = request.UserIds[1] } + }; + + _mockTaskAssignmentService.Setup(s => s.BulkAssignUsersToTaskAsync(request)) + .ReturnsAsync(expectedResponses); + + // Act + var result = await _controller.BulkAssignUsersToTask(request); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.BulkAssignUsersToTaskAsync(request), Times.Once); + } + + [Fact] + public async Task BulkAssignUsersToTask_ServiceThrowsException_ReturnsBadRequest() + { + // Arrange + var request = new BulkAssignUsersRequest + { + TaskId = Guid.NewGuid(), + UserIds = new List { Guid.NewGuid() } + }; + + _mockTaskAssignmentService.Setup(s => s.BulkAssignUsersToTaskAsync(request)) + .ThrowsAsync(new Exception("Error assigning users")); + + // Act + var result = await _controller.BulkAssignUsersToTask(request); + + // Assert + Assert.IsType(result); + } + + #endregion + + #region RemoveUserFromTask Tests + + [Fact] + public async Task RemoveUserFromTask_ValidRequest_ReturnsOkResult() + { + // Arrange + var request = new RemoveUserFromTaskRequest + { + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + _mockTaskAssignmentService.Setup(s => s.RemoveUserFromTaskAsync(request)) + .ReturnsAsync(true); + + // Act + var result = await _controller.RemoveUserFromTask(request); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.RemoveUserFromTaskAsync(request), Times.Once); + } + + [Fact] + public async Task RemoveUserFromTask_AssignmentNotFound_ReturnsBadRequest() + { + // Arrange + var request = new RemoveUserFromTaskRequest + { + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + _mockTaskAssignmentService.Setup(s => s.RemoveUserFromTaskAsync(request)) + .ThrowsAsync(new NotFoundException("Assignment not found")); + + // Act + var result = await _controller.RemoveUserFromTask(request); + + // Assert + Assert.IsType(result); + } + + #endregion + + #region GetAssignmentsByTaskId Tests + + [Fact] + public async Task GetAssignmentsByTaskId_ValidTaskId_ReturnsOkResult() + { + // Arrange + var taskId = Guid.NewGuid(); + var expectedAssignments = new List + { + new() { Id = Guid.NewGuid(), TaskId = taskId, UserId = Guid.NewGuid() }, + new() { Id = Guid.NewGuid(), TaskId = taskId, UserId = Guid.NewGuid() } + }; + + _mockTaskAssignmentService.Setup(s => s.GetAssignmentsByTaskIdAsync(taskId)) + .ReturnsAsync(expectedAssignments); + + // Act + var result = await _controller.GetAssignmentsByTaskId(taskId); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.GetAssignmentsByTaskIdAsync(taskId), Times.Once); + } + + [Fact] + public async Task GetAssignmentsByTaskId_ServiceThrowsException_ReturnsBadRequest() + { + // Arrange + var taskId = Guid.NewGuid(); + + _mockTaskAssignmentService.Setup(s => s.GetAssignmentsByTaskIdAsync(taskId)) + .ThrowsAsync(new Exception("Error retrieving assignments")); + + // Act + var result = await _controller.GetAssignmentsByTaskId(taskId); + + // Assert + Assert.IsType(result); + } + + #endregion + + #region GetAssignmentsByUserId Tests + + [Fact] + public async Task GetAssignmentsByUserId_ValidUserId_ReturnsOkResult() + { + // Arrange + var userId = Guid.NewGuid(); + var expectedAssignments = new List + { + new() { Id = Guid.NewGuid(), TaskId = Guid.NewGuid(), UserId = userId }, + new() { Id = Guid.NewGuid(), TaskId = Guid.NewGuid(), UserId = userId } + }; + + _mockTaskAssignmentService.Setup(s => s.GetAssignmentsByUserIdAsync(userId)) + .ReturnsAsync(expectedAssignments); + + // Act + var result = await _controller.GetAssignmentsByUserId(userId); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.GetAssignmentsByUserIdAsync(userId), Times.Once); + } + + #endregion + + #region GetMyAssignments Tests + + [Fact] + public async Task GetMyAssignments_ValidRequest_ReturnsOkResult() + { + // Arrange + var expectedAssignments = new List + { + new() { Id = Guid.NewGuid(), TaskId = Guid.NewGuid(), UserId = Guid.NewGuid() } + }; + + _mockTaskAssignmentService.Setup(s => s.GetMyAssignmentsAsync()) + .ReturnsAsync(expectedAssignments); + + // Act + var result = await _controller.GetMyAssignments(); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.GetMyAssignmentsAsync(), Times.Once); + } + + [Fact] + public async Task GetMyAssignments_ServiceThrowsException_ReturnsBadRequest() + { + // Arrange + _mockTaskAssignmentService.Setup(s => s.GetMyAssignmentsAsync()) + .ThrowsAsync(new UnauthorizedException("User not authenticated")); + + // Act + var result = await _controller.GetMyAssignments(); + + // Assert + Assert.IsType(result); + } + + #endregion + + #region GetAssignmentById Tests + + [Fact] + public async Task GetAssignmentById_ValidId_ReturnsOkResult() + { + // Arrange + var assignmentId = Guid.NewGuid(); + var expectedAssignment = new TaskAssignmentResponse + { + Id = assignmentId, + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + _mockTaskAssignmentService.Setup(s => s.GetAssignmentByIdAsync(assignmentId)) + .ReturnsAsync(expectedAssignment); + + // Act + var result = await _controller.GetAssignmentById(assignmentId); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.GetAssignmentByIdAsync(assignmentId), Times.Once); + } + + [Fact] + public async Task GetAssignmentById_NotFound_ReturnsBadRequest() + { + // Arrange + var assignmentId = Guid.NewGuid(); + + _mockTaskAssignmentService.Setup(s => s.GetAssignmentByIdAsync(assignmentId)) + .ThrowsAsync(new NotFoundException("Assignment not found")); + + // Act + var result = await _controller.GetAssignmentById(assignmentId); + + // Assert + Assert.IsType(result); + } + + #endregion + + #region IsUserAssignedToTask Tests + + [Fact] + public async Task IsUserAssignedToTask_UserAssigned_ReturnsOkResult() + { + // Arrange + var taskId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + + _mockTaskAssignmentService.Setup(s => s.IsUserAssignedToTaskAsync(taskId, userId)) + .ReturnsAsync(true); + + // Act + var result = await _controller.IsUserAssignedToTask(taskId, userId); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.IsUserAssignedToTaskAsync(taskId, userId), Times.Once); + } + + [Fact] + public async Task IsUserAssignedToTask_UserNotAssigned_ReturnsOkResult() + { + // Arrange + var taskId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + + _mockTaskAssignmentService.Setup(s => s.IsUserAssignedToTaskAsync(taskId, userId)) + .ReturnsAsync(false); + + // Act + var result = await _controller.IsUserAssignedToTask(taskId, userId); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + } + + #endregion + + #region RemoveAllAssignmentsForTask Tests + + [Fact] + public async Task RemoveAllAssignmentsForTask_ValidTaskId_ReturnsOkResult() + { + // Arrange + var taskId = Guid.NewGuid(); + + _mockTaskAssignmentService.Setup(s => s.RemoveAllAssignmentsForTaskAsync(taskId)) + .ReturnsAsync(true); + + // Act + var result = await _controller.RemoveAllAssignmentsForTask(taskId); + + // Assert + var okResult = Assert.IsType(result); + Assert.NotNull(okResult.Value); + _mockTaskAssignmentService.Verify(s => s.RemoveAllAssignmentsForTaskAsync(taskId), Times.Once); + } + + [Fact] + public async Task RemoveAllAssignmentsForTask_ServiceThrowsException_ReturnsBadRequest() + { + // Arrange + var taskId = Guid.NewGuid(); + + _mockTaskAssignmentService.Setup(s => s.RemoveAllAssignmentsForTaskAsync(taskId)) + .ThrowsAsync(new Exception("Error removing assignments")); + + // Act + var result = await _controller.RemoveAllAssignmentsForTask(taskId); + + // Assert + Assert.IsType(result); + } + + #endregion +} diff --git a/TaskGenix.Tests.Unit/Controllers/UserControllerTest.cs b/TaskGenix.Tests.Unit/Controllers/UserControllerTest.cs new file mode 100644 index 0000000..faa535f --- /dev/null +++ b/TaskGenix.Tests.Unit/Controllers/UserControllerTest.cs @@ -0,0 +1,600 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Moq; +using TaskGenix.BLL.Services.Implements; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.Core.Responses.User; +using TaskGenix.DAL.Data; +using TaskGenix.Repositories.Interfaces; +using TaskGenix.DAL.Repositories.Interfaces; +using TaskGenix.Tests.Unit.Common; +using Xunit; +using System.Linq.Expressions; + +namespace TaskGenix.Tests.Unit.Services; + +public class UserServiceTest : IDisposable +{ + private readonly Mock> _mockUnitOfWork; + private readonly Mock> _mockLogger; + private readonly Mock _mockHttpContextAccessor; + private readonly Mock _mockMapper; + private readonly Mock> _mockUserRepository; + private readonly UserService _userService; + private bool _disposed; + + public UserServiceTest() + { + _mockUnitOfWork = new Mock>(); + _mockLogger = new Mock>(); + _mockHttpContextAccessor = new Mock(); + _mockMapper = new Mock(); + _mockUserRepository = new Mock>(); + + _mockUnitOfWork.Setup(u => u.GetRepository()).Returns(_mockUserRepository.Object); + + _userService = new UserService( + _mockUnitOfWork.Object, + _mockLogger.Object, + _mockHttpContextAccessor.Object, + _mockMapper.Object); + } + + public void Dispose() + { + if (!_disposed) + { + _mockUnitOfWork?.Reset(); + _mockLogger?.Reset(); + _mockHttpContextAccessor?.Reset(); + _mockMapper?.Reset(); + _mockUserRepository?.Reset(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } + + #region GetAllUsers Tests + + [Fact] + public async Task GetAllUsers_ReturnsAllUsers_Successfully() + { + // Arrange + var expectedResponses = new List + { + new() { Id = Guid.NewGuid(), Username = "user1", Email = "user1@example.com" }, + new() { Id = Guid.NewGuid(), Username = "user2", Email = "user2@example.com" } + }; + + // Mock the GetListAsync call that uses selector (Expression>) + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(expectedResponses); + + // Act + var result = await _userService.GetAllUsers(); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count()); + Assert.Contains(result, r => r.Username == "user1"); + Assert.Contains(result, r => r.Username == "user2"); + } + + [Fact] + public async Task GetAllUsers_EmptyDatabase_ReturnsEmptyList() + { + // Arrange + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List()); + + // Act + var result = await _userService.GetAllUsers(); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + #endregion + + #region GetUserById Tests + + [Fact] + public async Task GetUserById_ExistingUser_ReturnsUser() + { + // Arrange + var userId = Guid.NewGuid(); + var user = TestDataBuilder.CreateTestUser(); + user.Id = userId; + + var expectedResponse = new UserResponse + { + Id = user.Id, + Username = user.Username, + Email = user.Email + }; + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(user); + + _mockMapper.Setup(m => m.Map(user)).Returns(expectedResponse); + + // Act + var result = await _userService.GetUserById(userId); + + // Assert + Assert.NotNull(result); + Assert.Equal(userId, result.Id); + Assert.Equal("testuser", result.Username); + Assert.Equal("test@example.com", result.Email); + } + + [Fact] + public async Task GetUserById_NonExistingUser_ThrowsNotFoundException() + { + // Arrange + var userId = Guid.NewGuid(); + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync((User?)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.GetUserById(userId)); + + Assert.Equal("User not found", exception.Message); + } + + #endregion + + #region CreateUser Tests + + [Fact] + public async Task CreateUser_ValidUser_CreatesSuccessfully() + { + // Arrange + var userRequest = new UserRequest + { + Username = "newuser", + Email = "newuser@example.com", + Password = "password123" + }; + + var expectedResponse = new UserResponse + { + Id = Guid.NewGuid(), + Username = userRequest.Username, + Email = userRequest.Email + }; + + // Mock GetListAsync to return empty list (no existing users) + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List()); + + _mockUserRepository.Setup(r => r.Insert(It.IsAny())).ReturnsAsync(true); + _mockUnitOfWork.Setup(u => u.CommitAsync()).ReturnsAsync(1); + _mockMapper.Setup(m => m.Map(It.IsAny())).Returns(expectedResponse); + + // Act + var result = await _userService.CreateUser(userRequest); + + // Assert + Assert.NotNull(result); + Assert.Equal(userRequest.Username, result.Username); + Assert.Equal(userRequest.Email, result.Email); + _mockUserRepository.Verify(r => r.Insert(It.IsAny()), Times.Once); + _mockUnitOfWork.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task CreateUser_DuplicateEmail_ThrowsInvalidOperationException() + { + // Arrange + var userRequest = new UserRequest + { + Username = "newuser", + Email = "existing@example.com", + Password = "password123" + }; + + var existingUser = TestDataBuilder.CreateTestUser("existing@example.com", "existinguser"); + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List { existingUser }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.CreateUser(userRequest)); + + Assert.Equal("User with this email or username already exists", exception.Message); + _mockUserRepository.Verify(r => r.Insert(It.IsAny()), Times.Never); + } + + [Fact] + public async Task CreateUser_DuplicateUsername_ThrowsInvalidOperationException() + { + // Arrange + var userRequest = new UserRequest + { + Username = "existinguser", + Email = "new@example.com", + Password = "password123" + }; + + var existingUser = TestDataBuilder.CreateTestUser("existing@example.com", "existinguser"); + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List { existingUser }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.CreateUser(userRequest)); + + Assert.Equal("User with this email or username already exists", exception.Message); + _mockUserRepository.Verify(r => r.Insert(It.IsAny()), Times.Never); + } + + [Fact] + public async Task CreateUser_NullUsername_ThrowsArgumentException() + { + // Arrange + var userRequest = new UserRequest + { + Username = null, + Email = "test@example.com", + Password = "password123" + }; + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.CreateUser(userRequest)); + + Assert.Equal("Username is required", exception.Message); + } + + [Fact] + public async Task CreateUser_NullEmail_ThrowsArgumentException() + { + // Arrange + var userRequest = new UserRequest + { + Username = "testuser", + Email = null, + Password = "password123" + }; + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.CreateUser(userRequest)); + + Assert.Equal("Email is required", exception.Message); + } + + [Fact] + public async Task CreateUser_NullPassword_ThrowsArgumentException() + { + // Arrange + var userRequest = new UserRequest + { + Username = "testuser", + Email = "test@example.com", + Password = null + }; + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List()); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.CreateUser(userRequest)); + + Assert.Equal("Password is required", exception.Message); + } + + #endregion + + #region Update User Tests (PUT - Full Update) + + [Fact] + public async Task UpdateUser_ValidFullUpdate_UpdatesSuccessfully() + { + // Arrange + var userId = Guid.NewGuid(); + var existingUser = TestDataBuilder.CreateTestUser("old@example.com", "olduser"); + existingUser.Id = userId; + + var updateRequest = new UpdateUserRequest + { + Username = "newusername", + Email = "new@example.com", + Password = "newpassword123" + }; + + var expectedResponse = new UserResponse + { + Id = userId, + Username = updateRequest.Username, + Email = updateRequest.Email + }; + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(existingUser); + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List { existingUser }); + + _mockUserRepository.Setup(r => r.Update(It.IsAny())).ReturnsAsync(true); + _mockUnitOfWork.Setup(u => u.CommitAsync()).ReturnsAsync(1); + _mockMapper.Setup(m => m.Map(It.IsAny())).Returns(expectedResponse); + } + + #endregion + + + #region UpdateUser Tests (PATCH - Partial Update) + + [Fact] + public async Task UpdateUser_ValidUpdate_UpdatesSuccessfully() + { + // Arrange + var userId = Guid.NewGuid(); + var existingUser = TestDataBuilder.CreateTestUser("old@example.com", "olduser"); + existingUser.Id = userId; + + var updateRequest = new UpdatePartialUserRequest + { + Username = "newusername", + Email = "new@example.com", + Password = "newpassword123" + }; + + var expectedResponse = new UserResponse + { + Id = userId, + Username = updateRequest.Username, + Email = updateRequest.Email + }; + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(existingUser); + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List { existingUser }); + + _mockUserRepository.Setup(r => r.Update(It.IsAny())).ReturnsAsync(true); + _mockUnitOfWork.Setup(u => u.CommitAsync()).ReturnsAsync(1); + _mockMapper.Setup(m => m.Map(It.IsAny())).Returns(expectedResponse); + + // Act + var result = await _userService.UpdatePartialUser(userId, updateRequest); + + // Assert + Assert.NotNull(result); + Assert.Equal(updateRequest.Username, result.Username); + Assert.Equal(updateRequest.Email, result.Email); + _mockUserRepository.Verify(r => r.Update(It.IsAny()), Times.Once); + _mockUnitOfWork.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task UpdateUser_NonExistingUser_ThrowsNotFoundException() + { + // Arrange + var userId = Guid.NewGuid(); + var updateRequest = new UpdatePartialUserRequest + { + Username = "newusername", + Email = "new@example.com", + Password = "newpassword123" + }; + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync((User?)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.UpdatePartialUser(userId, updateRequest)); + + Assert.Equal("User not found", exception.Message); + _mockUserRepository.Verify(r => r.Update(It.IsAny()), Times.Never); + } + + [Fact] + public async Task UpdateUser_DuplicateEmailFromAnotherUser_ThrowsInvalidOperationException() + { + // Arrange + var userId = Guid.NewGuid(); + var existingUser = TestDataBuilder.CreateTestUser("original@example.com", "originaluser"); + existingUser.Id = userId; + + var anotherUser = TestDataBuilder.CreateTestUser("taken@example.com", "anotheruser"); + anotherUser.Id = Guid.NewGuid(); + + var updateRequest = new UpdatePartialUserRequest + { + Email = "taken@example.com" // This email is already taken by another user + }; + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(existingUser); + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List { existingUser, anotherUser }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.UpdatePartialUser(userId, updateRequest)); + + Assert.Equal("Email is already taken by another user", exception.Message); + _mockUserRepository.Verify(r => r.Update(It.IsAny()), Times.Never); + } + + [Fact] + public async Task UpdateUser_DuplicateUsernameFromAnotherUser_ThrowsInvalidOperationException() + { + // Arrange + var userId = Guid.NewGuid(); + var existingUser = TestDataBuilder.CreateTestUser("original@example.com", "originaluser"); + existingUser.Id = userId; + + var anotherUser = TestDataBuilder.CreateTestUser("another@example.com", "takenusername"); + anotherUser.Id = Guid.NewGuid(); + + var updateRequest = new UpdatePartialUserRequest + { + Username = "takenusername" // This username is already taken by another user + }; + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(existingUser); + + _mockUserRepository.Setup(r => r.GetListAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>(), + It.IsAny())) + .ReturnsAsync(new List { existingUser, anotherUser }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.UpdatePartialUser(userId, updateRequest)); + + Assert.Equal("Username is already taken by another user", exception.Message); + _mockUserRepository.Verify(r => r.Update(It.IsAny()), Times.Never); + } + + #endregion + + #region DeleteUser Tests + + [Fact] + public async Task DeleteUser_ExistingUser_SoftDeletesSuccessfully() + { + // Arrange + var userId = Guid.NewGuid(); + var existingUser = TestDataBuilder.CreateTestUser("test@example.com", "testuser"); + existingUser.Id = userId; + existingUser.IsActive = true; + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(existingUser); + + _mockUserRepository.Setup(r => r.Update(It.IsAny())).ReturnsAsync(true); + _mockUnitOfWork.Setup(u => u.CommitAsync()).ReturnsAsync(1); + + // Act + var result = await _userService.DeleteUser(userId); + + // Assert + Assert.True(result); + Assert.False(existingUser.IsActive); // Should be soft deleted + _mockUserRepository.Verify(r => r.Update(It.IsAny()), Times.Once); + _mockUnitOfWork.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task DeleteUser_NonExistingUser_ThrowsNotFoundException() + { + // Arrange + var userId = Guid.NewGuid(); + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync((User?)null); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _userService.DeleteUser(userId)); + + Assert.Equal("User not found", exception.Message); + _mockUserRepository.Verify(r => r.Update(It.IsAny()), Times.Never); + _mockUnitOfWork.Verify(u => u.CommitAsync(), Times.Never); + } + + #endregion +} \ No newline at end of file diff --git a/TaskGenix.Tests.Unit/Controllers/UserDeviceTokenControllerTest.cs b/TaskGenix.Tests.Unit/Controllers/UserDeviceTokenControllerTest.cs new file mode 100644 index 0000000..e69de29 diff --git a/TaskGenix.Tests.Unit/Services/TaskAssignmentServiceTest.cs b/TaskGenix.Tests.Unit/Services/TaskAssignmentServiceTest.cs new file mode 100644 index 0000000..d0dc428 --- /dev/null +++ b/TaskGenix.Tests.Unit/Services/TaskAssignmentServiceTest.cs @@ -0,0 +1,605 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MapsterMapper; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Moq; +using TaskGenix.BLL.Services.Implements; +using TaskGenix.Core.Domains; +using TaskGenix.Core.Exceptions; +using TaskGenix.Core.Requests.TaskAssignment; +using TaskGenix.Core.Responses.TaskGen; +using TaskGenix.DAL.Data; +using TaskGenix.DAL.Repositories.Interfaces; +using TaskGenix.Repositories.Interfaces; +using Xunit; + +namespace TaskGenix.Tests.Unit.Services; + +public class TaskAssignmentServiceTest : IDisposable +{ + private readonly Mock> _mockUnitOfWork; + private readonly Mock> _mockLogger; + private readonly Mock _mockHttpContextAccessor; + private readonly Mock _mockMapper; + private readonly Mock _mockTaskAssignmentRepository; + private readonly Mock> _mockTaskRepository; + private readonly Mock> _mockUserRepository; + private readonly Mock> _mockProjectMemberRepository; + private readonly TaskAssignmentService _taskAssignmentService; + private bool _disposed; + + public TaskAssignmentServiceTest() + { + _mockUnitOfWork = new Mock>(); + _mockLogger = new Mock>(); + _mockHttpContextAccessor = new Mock(); + _mockMapper = new Mock(); + _mockTaskAssignmentRepository = new Mock(); + _mockTaskRepository = new Mock>(); + _mockUserRepository = new Mock>(); + _mockProjectMemberRepository = new Mock>(); + + _mockUnitOfWork.Setup(u => u.GetRepository()) + .Returns(_mockTaskAssignmentRepository.Object); + _mockUnitOfWork.Setup(u => u.GetRepository()) + .Returns(_mockTaskRepository.Object); + _mockUnitOfWork.Setup(u => u.GetRepository()) + .Returns(_mockUserRepository.Object); + _mockUnitOfWork.Setup(u => u.GetRepository()) + .Returns(_mockProjectMemberRepository.Object); + + _taskAssignmentService = new TaskAssignmentService( + _mockUnitOfWork.Object, + _mockLogger.Object, + _mockHttpContextAccessor.Object, + _mockMapper.Object); + } + + public void Dispose() + { + if (!_disposed) + { + _mockUnitOfWork?.Reset(); + _mockLogger?.Reset(); + _mockHttpContextAccessor?.Reset(); + _mockMapper?.Reset(); + _mockTaskAssignmentRepository?.Reset(); + _mockTaskRepository?.Reset(); + _mockUserRepository?.Reset(); + _mockProjectMemberRepository?.Reset(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } + + #region AssignUserToTask Tests + + [Fact] + public async Task AssignUserToTask_ValidRequest_ReturnsAssignment() + { + // Arrange + var taskId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var projectId = Guid.NewGuid(); + var request = new AssignUserToTaskRequest { TaskId = taskId, UserId = userId }; + + var project = new Project { Id = projectId, Name = "Test Project", Description = "", OwnerId = Guid.NewGuid() }; + var task = new TaskGen { Id = taskId, Title = "Test Task", ProjectId = projectId, Project = project }; + var user = new User { Id = userId, Username = "testuser", Email = "test@example.com", PasswordHash = "hash", Role = UserRole.User, IsActive = true }; + var projectMember = new ProjectMember { Id = Guid.NewGuid(), ProjectId = projectId, UserId = userId, User = user, Project = project }; + var assignment = new TaskAssignment + { + Id = Guid.NewGuid(), + TaskId = taskId, + UserId = userId, + AssignedAt = DateTime.UtcNow + }; + var expectedResponse = new TaskAssignmentResponse + { + Id = assignment.Id, + TaskId = taskId, + UserId = userId, + AssignedAt = assignment.AssignedAt + }; + + _mockTaskRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(task); + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(user); + + _mockProjectMemberRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(projectMember); + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentByTaskAndUserAsync(taskId, userId)) + .ReturnsAsync((TaskAssignment?)null); + + _mockTaskAssignmentRepository.Setup(r => r.Insert(It.IsAny())) + .ReturnsAsync(true); + + _mockUnitOfWork.Setup(u => u.CommitAsync()) + .ReturnsAsync(1); + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentWithDetailsAsync(It.IsAny())) + .ReturnsAsync(assignment); + + _mockMapper.Setup(m => m.Map(It.IsAny())) + .Returns(expectedResponse); + + // Act + var result = await _taskAssignmentService.AssignUserToTaskAsync(request); + + // Assert + Assert.NotNull(result); + Assert.Equal(taskId, result.TaskId); + Assert.Equal(userId, result.UserId); + _mockTaskAssignmentRepository.Verify(r => r.Insert(It.IsAny()), Times.Once); + _mockUnitOfWork.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task AssignUserToTask_TaskNotFound_ThrowsNotFoundException() + { + // Arrange + var request = new AssignUserToTaskRequest + { + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + _mockTaskRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync((TaskGen?)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + _taskAssignmentService.AssignUserToTaskAsync(request)); + } + + [Fact] + public async Task AssignUserToTask_UserNotFound_ThrowsNotFoundException() + { + // Arrange + var request = new AssignUserToTaskRequest + { + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + var task = new TaskGen { Id = request.TaskId, Title = "Test Task" }; + + _mockTaskRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(task); + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync((User?)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + _taskAssignmentService.AssignUserToTaskAsync(request)); + } + + [Fact] + public async Task AssignUserToTask_UserAlreadyAssigned_ThrowsConflictException() + { + // Arrange + var taskId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var projectId = Guid.NewGuid(); + var request = new AssignUserToTaskRequest { TaskId = taskId, UserId = userId }; + + var project = new Project { Id = projectId, Name = "Test Project", Description = "", OwnerId = Guid.NewGuid() }; + var task = new TaskGen { Id = taskId, Title = "Test Task", ProjectId = projectId, Project = project }; + var user = new User { Id = userId, Username = "testuser", Email = "test@example.com", PasswordHash = "hash", Role = UserRole.User, IsActive = true }; + var projectMember = new ProjectMember { Id = Guid.NewGuid(), ProjectId = projectId, UserId = userId, User = user, Project = project }; + var existingAssignment = new TaskAssignment + { + Id = Guid.NewGuid(), + TaskId = taskId, + UserId = userId + }; + + _mockTaskRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(task); + + _mockUserRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(user); + + _mockProjectMemberRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(projectMember); + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentByTaskAndUserAsync(taskId, userId)) + .ReturnsAsync(existingAssignment); + + // Act & Assert + await Assert.ThrowsAsync(() => + _taskAssignmentService.AssignUserToTaskAsync(request)); + } + + #endregion + + #region BulkAssignUsersToTask Tests + + [Fact] + public async Task BulkAssignUsersToTask_ValidRequest_ReturnsAssignments() + { + // Arrange + var taskId = Guid.NewGuid(); + var userId1 = Guid.NewGuid(); + var userId2 = Guid.NewGuid(); + var projectId = Guid.NewGuid(); + var request = new BulkAssignUsersRequest { TaskId = taskId, UserIds = new List { userId1, userId2 } }; + + var project = new Project { Id = projectId, Name = "Test Project", Description = "", OwnerId = Guid.NewGuid() }; + var task = new TaskGen { Id = taskId, Title = "Test Task", ProjectId = projectId, Project = project }; + var user1 = new User { Id = userId1, Username = "user1", Email = "user1@example.com", PasswordHash = "hash", Role = UserRole.User, IsActive = true }; + var user2 = new User { Id = userId2, Username = "user2", Email = "user2@example.com", PasswordHash = "hash", Role = UserRole.User, IsActive = true }; + var projectMember1 = new ProjectMember { Id = Guid.NewGuid(), ProjectId = projectId, UserId = userId1, User = user1, Project = project }; + var projectMember2 = new ProjectMember { Id = Guid.NewGuid(), ProjectId = projectId, UserId = userId2, User = user2, Project = project }; + + _mockTaskRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(task); + + // Setup for both users + _mockUserRepository.SetupSequence(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(user1) + .ReturnsAsync(user2); + + // Setup project member validation for both users + _mockProjectMemberRepository.SetupSequence(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync(projectMember1) + .ReturnsAsync(projectMember2); + + _mockTaskAssignmentRepository.Setup(r => r.IsUserAssignedToTaskAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + _mockTaskAssignmentRepository.Setup(r => r.Insert(It.IsAny())) + .ReturnsAsync(true); + + _mockUnitOfWork.Setup(u => u.CommitAsync()) + .ReturnsAsync(1); + + var assignment1 = new TaskAssignment { Id = Guid.NewGuid(), TaskId = taskId, UserId = userId1, AssignedAt = DateTime.UtcNow }; + var assignment2 = new TaskAssignment { Id = Guid.NewGuid(), TaskId = taskId, UserId = userId2, AssignedAt = DateTime.UtcNow }; + + _mockTaskAssignmentRepository.SetupSequence(r => r.GetAssignmentWithDetailsAsync(It.IsAny())) + .ReturnsAsync(assignment1) + .ReturnsAsync(assignment2); + + _mockMapper.Setup(m => m.Map(It.IsAny())) + .Returns((TaskAssignment src) => new TaskAssignmentResponse + { + Id = src.Id, + TaskId = src.TaskId, + UserId = src.UserId, + AssignedAt = src.AssignedAt + }); + + // Act + var result = await _taskAssignmentService.BulkAssignUsersToTaskAsync(request); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + } + + [Fact] + public async Task BulkAssignUsersToTask_TaskNotFound_ThrowsNotFoundException() + { + // Arrange + var request = new BulkAssignUsersRequest + { + TaskId = Guid.NewGuid(), + UserIds = new List { Guid.NewGuid() } + }; + + _mockTaskRepository.Setup(r => r.FirstOrDefaultAsync( + It.IsAny>>(), + It.IsAny, IOrderedQueryable>>(), + It.IsAny, IQueryable>>())) + .ReturnsAsync((TaskGen?)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + _taskAssignmentService.BulkAssignUsersToTaskAsync(request)); + } + + #endregion + + #region RemoveUserFromTask Tests + + [Fact] + public async Task RemoveUserFromTask_ValidRequest_ReturnsTrue() + { + // Arrange + var taskId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var request = new RemoveUserFromTaskRequest { TaskId = taskId, UserId = userId }; + + var assignment = new TaskAssignment + { + Id = Guid.NewGuid(), + TaskId = taskId, + UserId = userId + }; + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentByTaskAndUserAsync(taskId, userId)) + .ReturnsAsync(assignment); + + _mockTaskAssignmentRepository.Setup(r => r.Delete(It.IsAny())) + .ReturnsAsync(true); + + _mockUnitOfWork.Setup(u => u.CommitAsync()) + .ReturnsAsync(1); + + // Act + var result = await _taskAssignmentService.RemoveUserFromTaskAsync(request); + + // Assert + Assert.True(result); + _mockTaskAssignmentRepository.Verify(r => r.Delete(assignment), Times.Once); + _mockUnitOfWork.Verify(u => u.CommitAsync(), Times.Once); + } + + [Fact] + public async Task RemoveUserFromTask_AssignmentNotFound_ThrowsNotFoundException() + { + // Arrange + var request = new RemoveUserFromTaskRequest + { + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentByTaskAndUserAsync( + request.TaskId, request.UserId)) + .ReturnsAsync((TaskAssignment?)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + _taskAssignmentService.RemoveUserFromTaskAsync(request)); + } + + #endregion + + #region GetAssignmentsByTaskId Tests + + [Fact] + public async Task GetAssignmentsByTaskId_ValidTaskId_ReturnsAssignments() + { + // Arrange + var taskId = Guid.NewGuid(); + var assignments = new List + { + new() { Id = Guid.NewGuid(), TaskId = taskId, UserId = Guid.NewGuid() }, + new() { Id = Guid.NewGuid(), TaskId = taskId, UserId = Guid.NewGuid() } + }; + + var responses = assignments.Select(a => new TaskAssignmentResponse + { + Id = a.Id, + TaskId = a.TaskId, + UserId = a.UserId + }).ToList(); + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentsByTaskIdAsync(taskId)) + .ReturnsAsync(assignments); + + _mockMapper.Setup(m => m.Map>(assignments)) + .Returns(responses); + + // Act + var result = await _taskAssignmentService.GetAssignmentsByTaskIdAsync(taskId); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + } + + [Fact] + public async Task GetAssignmentsByTaskId_NoAssignments_ReturnsEmptyList() + { + // Arrange + var taskId = Guid.NewGuid(); + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentsByTaskIdAsync(taskId)) + .ReturnsAsync(new List()); + + _mockMapper.Setup(m => m.Map>(It.IsAny>())) + .Returns(new List()); + + // Act + var result = await _taskAssignmentService.GetAssignmentsByTaskIdAsync(taskId); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + #endregion + + #region GetAssignmentsByUserId Tests + + [Fact] + public async Task GetAssignmentsByUserId_ValidUserId_ReturnsAssignments() + { + // Arrange + var userId = Guid.NewGuid(); + var assignments = new List + { + new() { Id = Guid.NewGuid(), TaskId = Guid.NewGuid(), UserId = userId }, + new() { Id = Guid.NewGuid(), TaskId = Guid.NewGuid(), UserId = userId } + }; + + var responses = assignments.Select(a => new TaskAssignmentResponse + { + Id = a.Id, + TaskId = a.TaskId, + UserId = a.UserId + }).ToList(); + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentsByUserIdAsync(userId)) + .ReturnsAsync(assignments); + + _mockMapper.Setup(m => m.Map>(assignments)) + .Returns(responses); + + // Act + var result = await _taskAssignmentService.GetAssignmentsByUserIdAsync(userId); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + } + + #endregion + + #region GetAssignmentById Tests + + [Fact] + public async Task GetAssignmentById_ValidId_ReturnsAssignment() + { + // Arrange + var assignmentId = Guid.NewGuid(); + var assignment = new TaskAssignment + { + Id = assignmentId, + TaskId = Guid.NewGuid(), + UserId = Guid.NewGuid() + }; + + var response = new TaskAssignmentResponse + { + Id = assignmentId, + TaskId = assignment.TaskId, + UserId = assignment.UserId + }; + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentWithDetailsAsync(assignmentId)) + .ReturnsAsync(assignment); + + _mockMapper.Setup(m => m.Map(assignment)) + .Returns(response); + + // Act + var result = await _taskAssignmentService.GetAssignmentByIdAsync(assignmentId); + + // Assert + Assert.NotNull(result); + Assert.Equal(assignmentId, result.Id); + } + + [Fact] + public async Task GetAssignmentById_NotFound_ThrowsNotFoundException() + { + // Arrange + var assignmentId = Guid.NewGuid(); + + _mockTaskAssignmentRepository.Setup(r => r.GetAssignmentWithDetailsAsync(assignmentId)) + .ReturnsAsync((TaskAssignment?)null); + + // Act & Assert + await Assert.ThrowsAsync(() => + _taskAssignmentService.GetAssignmentByIdAsync(assignmentId)); + } + + #endregion + + #region IsUserAssignedToTask Tests + + [Fact] + public async Task IsUserAssignedToTask_UserAssigned_ReturnsTrue() + { + // Arrange + var taskId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + + _mockTaskAssignmentRepository.Setup(r => r.IsUserAssignedToTaskAsync(taskId, userId)) + .ReturnsAsync(true); + + // Act + var result = await _taskAssignmentService.IsUserAssignedToTaskAsync(taskId, userId); + + // Assert + Assert.True(result); + } + + [Fact] + public async Task IsUserAssignedToTask_UserNotAssigned_ReturnsFalse() + { + // Arrange + var taskId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + + _mockTaskAssignmentRepository.Setup(r => r.IsUserAssignedToTaskAsync(taskId, userId)) + .ReturnsAsync(false); + + // Act + var result = await _taskAssignmentService.IsUserAssignedToTaskAsync(taskId, userId); + + // Assert + Assert.False(result); + } + + #endregion + + #region RemoveAllAssignmentsForTask Tests + + [Fact] + public async Task RemoveAllAssignmentsForTask_ValidTaskId_ReturnsTrue() + { + // Arrange + var taskId = Guid.NewGuid(); + + _mockTaskAssignmentRepository.Setup(r => r.RemoveAllAssignmentsForTaskAsync(taskId)) + .ReturnsAsync(true); + + // Act + var result = await _taskAssignmentService.RemoveAllAssignmentsForTaskAsync(taskId); + + // Assert + Assert.True(result); + _mockTaskAssignmentRepository.Verify(r => r.RemoveAllAssignmentsForTaskAsync(taskId), Times.Once); + } + + #endregion +} diff --git a/TaskGenix.Tests.Unit/TaskGenix.Tests.Unit.csproj b/TaskGenix.Tests.Unit/TaskGenix.Tests.Unit.csproj new file mode 100644 index 0000000..76d6370 --- /dev/null +++ b/TaskGenix.Tests.Unit/TaskGenix.Tests.Unit.csproj @@ -0,0 +1,36 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 0000000..89a1f74 --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,62 @@ +networks: + taskgenix_network: + driver: bridge + +volumes: + taskgenix.db_data: + +services: + taskgenix.db: + container_name: taskgenix-db + image: postgres:16-alpine + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + POSTGRES_DB: TaskGenixDB + PGDATA: /data/postgres + ports: + - "5433:5432" + networks: + - taskgenix_network + volumes: + - taskgenix.db_data:/data/postgres + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U admin -d TaskGenixDB" ] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + + pgadmin: + container_name: pgadmin_taskgenix + image: dpage/pgadmin4 + ports: + - "5051:80" + environment: + PGADMIN_DEFAULT_EMAIL: admin@admin.com + PGADMIN_DEFAULT_PASSWORD: admin + depends_on: + - taskgenix.db + networks: + - taskgenix_network + + server: + container_name: taskgenix-server + build: + context: . + dockerfile: TaskGenix.Server/Dockerfile + depends_on: + - taskgenix.db + - pgadmin + ports: + - "8080:5009" + - "8081:5009" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - JWT__SecretKey=your-super-secret-jwt-key-that-is-at-least-32-characters-long! + - JWT__Issuer=TaskGenix + - JWT__Audience=TaskGenix-Users + - ConnectionStrings__SupabaseConnection=Host=taskgenix.db;Database=TaskGenixDB;Username=admin;Password=admin;Port=5432 + networks: + - taskgenix_network + diff --git a/docker-compose-portainer.yml b/docker-compose-portainer.yml new file mode 100644 index 0000000..78d9481 --- /dev/null +++ b/docker-compose-portainer.yml @@ -0,0 +1,14 @@ +services: + server_prod: + container_name: taskgenix-server_v3 + image: lcaohoanq/taskgenix-be:latest + ports: + - "8386:5009" + environment: + - JWT__SecretKey=your-super-secret-jwt-key-that-is-at-least-32-characters-long! + - JWT__Issuer=TaskGenix + - JWT__Audience=TaskGenix-Users + - ConnectionStrings__SupabaseConnection=Host=aws-1-ap-southeast-1.pooler.supabase.com;Database=postgres;Username=postgres.xrhjxclsnjdqhxcvawsh;Password=Oiconsongque1;Port=5432 + volumes: + - ./firebase-service-account.json:/app/firebase-service-account.json:ro + diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000..bafe24d --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,29 @@ +services: + server_prod_local_build: + container_name: taskgenix-server_v3 + build: + context: . + dockerfile: ./Dockerfile.prod + ports: + - "8386:5009" + environment: + - JWT__SecretKey=your-super-secret-jwt-key-that-is-at-least-32-characters-long! + - JWT__Issuer=TaskGenix + - JWT__Audience=TaskGenix-Users + - ConnectionStrings__SupabaseConnection=Host=taskgenix.db;Database=TaskGenixDB;Username=admin;Password=admin;Port=5432 + volumes: + - ./firebase-service-account.json:/app/firebase-service-account.json:ro + + server_prod_published_image: + container_name: taskgenix-server_v2 + image: lcaohoanq/taskgenix-be:latest + ports: + - "8386:5009" + environment: + - JWT__SecretKey=your-super-secret-jwt-key-that-is-at-least-32-characters-long! + - JWT__Issuer=TaskGenix + - JWT__Audience=TaskGenix-Users + - ConnectionStrings__SupabaseConnection=Host=taskgenix.db;Database=TaskGenixDB;Username=admin;Password=admin;Port=5432 + volumes: + - ./firebase-service-account.json:/app/firebase-service-account.json:ro + diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 386b00c..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,39 +0,0 @@ -networks: - taskgenix_network: - driver: bridge - -services: - taskgenix.db: - container_name: taskgenix-db - image: postgres:latest - environment: - POSTGRES_USER: db_user - POSTGRES_PASSWORD: admin - POSTGRES_DB: TaskGenixDB - ports: - - "5432:5432" - networks: - - taskgenix_network - volumes: - - taskgenix.db_data:/var/lib/postgresql/data - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U db_user -d TaskGenixDB" ] - interval: 5s - timeout: 5s - retries: 5 - - pgadmin: - container_name: pgadmin_taskgenix - image: dpage/pgadmin4 - ports: - - "5050:80" - environment: - PGADMIN_DEFAULT_EMAIL: admin@admin.com - PGADMIN_DEFAULT_PASSWORD: admin - depends_on: - - taskgenix.db - networks: - - taskgenix_network - -volumes: - taskgenix.db_data: diff --git a/run-tests.sh b/run-tests.sh new file mode 100755 index 0000000..a1b658c --- /dev/null +++ b/run-tests.sh @@ -0,0 +1,89 @@ +#!/bin/bash + + +echo "🧪 Running TaskGenix Tests" +echo "===========================" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Function to print colored output +print_status() { + echo -e "${2}${1}${NC}" +} + +# Check if dotnet is installed +if ! command -v dotnet &> /dev/null; then + print_status "❌ .NET SDK is not installed or not in PATH" $RED + exit 1 +fi + +print_status "📋 .NET SDK version:" $YELLOW +dotnet --version + +# Change to the project root directory +cd "$(dirname "$0")" + +print_status "\n🔧 Restoring packages..." $YELLOW +dotnet restore + +if [ $? -ne 0 ]; then + print_status "❌ Package restoration failed" $RED + exit 1 +fi + +print_status "✅ Package restoration completed" $GREEN + +dotnet test TaskGenix.Tests.Unit/TaskGenix.Tests.Unit.csproj --verbosity minimal --logger "console;verbosity=detailed" +dotnet test TaskGenix.Tests.Integration/TaskGenix.Tests.Integration.csproj --filter "FullyQualifiedName~InMemory" --verbosity minimal --logger "console;verbosity=detailed" + +# Run Unit Tests +print_status "\n🧪 Running Unit Tests..." $YELLOW +print_status "=========================" $YELLOW +dotnet test TaskGenix.Tests.Unit/TaskGenix.Tests.Unit.csproj --verbosity minimal --logger "console;verbosity=detailed" +if [ $? -eq 0 ]; then + print_status "✅ Unit tests passed" $GREEN +else + print_status "❌ Unit tests failed" $RED + UNIT_TEST_FAILED=1 +fi + +# Run Integration Tests (all) +print_status "\n🧪 Running Integration Tests..." $YELLOW +print_status "==============================" $YELLOW +dotnet test TaskGenix.Tests.Integration/TaskGenix.Tests.Integration.csproj --verbosity minimal --logger "console;verbosity=detailed" +if [ $? -eq 0 ]; then + print_status "✅ Integration tests passed" $GREEN +else + print_status "❌ Integration tests failed" $RED + INTEGRATION_TEST_FAILED=1 +fi + +# Summary + +print_status "\n📊 Test Summary" $YELLOW +print_status "===============" $YELLOW + +if [ -z "$UNIT_TEST_FAILED" ]; then + print_status "✅ Unit Tests: PASSED" $GREEN +else + print_status "❌ Unit Tests: FAILED" $RED +fi + +if [ -z "$INTEGRATION_TEST_FAILED" ]; then + print_status "✅ Integration Tests: PASSED" $GREEN +else + print_status "❌ Integration Tests: FAILED" $RED +fi + +# Exit with error if any tests failed +if [ ! -z "$UNIT_TEST_FAILED" ] || [ ! -z "$INTEGRATION_TEST_FAILED" ]; then + print_status "\n❌ Some tests failed" $RED + exit 1 +else + print_status "\n🎉 All tests passed!" $GREEN + exit 0 +fi