diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 000000000..8ea0c1672
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,274 @@
+# EditorConfig é incrível: https://EditorConfig.org
+
+# Arquivo principal
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# Arquivos C# e .NET
+[*.{cs,csx,vb,vbx,csproj,fsproj,vbproj,props,targets}]
+indent_style = space
+indent_size = 4
+
+# Arquivos de configuração e dados
+[*.{json,js,jsx,ts,tsx,xml,yml,yaml}]
+indent_style = space
+indent_size = 2
+
+# Arquivos Markdown
+[*.md]
+trim_trailing_whitespace = false
+
+# Shell scripts
+[*.sh]
+end_of_line = lf
+
+# PowerShell scripts
+[*.ps1]
+end_of_line = lf
+
+# Docker files
+[Dockerfile]
+end_of_line = lf
+
+# =====================================
+# REGRAS DE ANÁLISE DE CÓDIGO C# - PRODUÇÃO
+# =====================================
+[*.cs]
+
+# Regras básicas de qualidade
+dotnet_diagnostic.CA1515.severity = none # Consider making public types internal
+dotnet_diagnostic.CA1848.severity = none # Use LoggerMessage delegates instead of LoggerExtensions methods
+
+# CRÍTICAS DE SEGURANÇA - RIGOROSAS EM PRODUÇÃO
+# CA2007: ConfigureAwait(false) - importante em bibliotecas, opcional em aplicações ASP.NET Core
+dotnet_diagnostic.CA2007.severity = suggestion # Consider calling ConfigureAwait on the awaited task
+
+# CA1031: Catch específico - importante para diagnóstico, mas permite exceções genéricas em pontos de entrada
+dotnet_diagnostic.CA1031.severity = suggestion # Modify to catch a more specific allowed exception type
+
+# CA1062: Validação de null - crítico para APIs públicas
+dotnet_diagnostic.CA1062.severity = warning # Validate parameter is non-null before using it
+
+# CA2000: Dispose de recursos - crítico para vazamentos de memória
+dotnet_diagnostic.CA2000.severity = warning # Call System.IDisposable.Dispose on object
+
+# CA5394: Random inseguro - CRÍTICO para segurança criptográfica
+dotnet_diagnostic.CA5394.severity = error # Random is an insecure random number generator
+
+# CA2100: SQL Injection - CRÍTICO para segurança de dados
+dotnet_diagnostic.CA2100.severity = error # Review if the query string accepts any user input
+
+# Parameter naming conflicts with reserved keywords
+dotnet_diagnostic.CA1716.severity = none # Rename parameter so that it no longer conflicts with reserved keywords
+
+# Regras de estilo menos críticas
+dotnet_diagnostic.CA1305.severity = none # Specify IFormatProvider
+dotnet_diagnostic.CA1307.severity = none # Specify StringComparison for clarity
+dotnet_diagnostic.CA1310.severity = none # Specify StringComparison for performance
+dotnet_diagnostic.CA1304.severity = none # Specify CultureInfo
+dotnet_diagnostic.CA1308.severity = none # Normalize strings to uppercase
+
+# Performance (sugestões)
+dotnet_diagnostic.CA1863.severity = suggestion # Cache CompositeFormat for repeated use
+dotnet_diagnostic.CA1869.severity = suggestion # Cache and reuse JsonSerializerOptions instances
+dotnet_diagnostic.CA1860.severity = suggestion # Prefer comparing Count to 0 rather than using Any()
+dotnet_diagnostic.CA1851.severity = suggestion # Possible multiple enumerations of IEnumerable
+dotnet_diagnostic.CA1859.severity = suggestion # Change return type for improved performance
+dotnet_diagnostic.CA1822.severity = suggestion # Member does not access instance data and can be marked as static
+
+# Type sealing e organização
+dotnet_diagnostic.CA1852.severity = none # Type can be sealed because it has no subtypes
+dotnet_diagnostic.CA1812.severity = none # Internal class that is apparently never instantiated
+dotnet_diagnostic.CA1050.severity = none # Declare types in namespaces (Program class)
+dotnet_diagnostic.CA1052.severity = none # Static holder types should be static (Program class)
+
+# Configurações de API
+dotnet_diagnostic.CA2227.severity = none # Collection properties should be read-only (configuration POCOs)
+dotnet_diagnostic.CA1002.severity = none # Do not expose generic lists (configuration POCOs)
+dotnet_diagnostic.CA1056.severity = none # Use Uri instead of string for URL properties (configuration classes)
+
+# Exception handling específico
+dotnet_diagnostic.CA1032.severity = none # Exception constructors (custom exceptions)
+dotnet_diagnostic.CA1040.severity = none # Avoid empty interfaces (marker interfaces)
+
+# Domain naming conventions
+dotnet_diagnostic.CA1720.severity = none # Identifiers contain type names (domain naming)
+dotnet_diagnostic.CA1711.severity = none # Types end with reserved suffixes (domain naming)
+dotnet_diagnostic.CA1724.severity = none # Type name conflicts with namespace name
+dotnet_diagnostic.CA1725.severity = none # Parameter name should match interface declaration
+
+# Operadores e conversões
+dotnet_diagnostic.CA2225.severity = none # Operator overloads provide named alternatives (value objects)
+dotnet_diagnostic.CA1866.severity = suggestion # Use char overloads for StartsWith
+dotnet_diagnostic.CA2234.severity = none # Use URI overload instead of string overload
+
+# Generics
+dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types
+dotnet_diagnostic.CA2955.severity = none # Use comparison to default(T) instead
+
+# =====================================
+# REGRAS ESPECÍFICAS PARA TESTES
+# =====================================
+[**/*Test*.cs,**/Tests/**/*.cs,**/tests/**/*.cs,**/MeAjudaAi.*.Tests/**/*.cs,**/Modules/**/Tests/**/*.cs]
+
+# Relaxar regras críticas APENAS em testes
+dotnet_diagnostic.CA2007.severity = none # ConfigureAwait não necessário em testes
+dotnet_diagnostic.CA1031.severity = none # Catch genérico OK em testes
+dotnet_diagnostic.CA1062.severity = none # Validação de null menos crítica em testes
+dotnet_diagnostic.CA5394.severity = suggestion # Random pode ser usado em dados de teste
+dotnet_diagnostic.CA2100.severity = suggestion # SQL dinâmico pode ser usado em testes
+
+# Nullable warnings em testes (intencionalmente testando cenários null)
+dotnet_diagnostic.CS8604.severity = none # Possible null reference argument - OK em testes de validação
+dotnet_diagnostic.CS8625.severity = none # Cannot convert null literal to non-nullable reference - OK em testes
+
+# Test-specific warnings (noise in test context)
+dotnet_diagnostic.CA1707.severity = none # Remove underscores from member names (common in test methods)
+dotnet_diagnostic.CA1303.severity = none # Use resource tables instead of literal strings (console logging in tests)
+dotnet_diagnostic.CA1054.severity = none # Use Uri instead of string for URL parameters (test helpers)
+dotnet_diagnostic.CA1816.severity = none # Call GC.SuppressFinalize in DisposeAsync (test infrastructure)
+dotnet_diagnostic.CA1311.severity = none # Specify culture for string operations (test data)
+dotnet_diagnostic.CA1823.severity = none # Unused fields (test assemblies)
+dotnet_diagnostic.CA1508.severity = none # Dead code conditions (test scenarios)
+dotnet_diagnostic.CA1034.severity = none # Do not nest types (test factories)
+dotnet_diagnostic.CA1051.severity = none # Do not declare visible instance fields (test fixtures)
+dotnet_diagnostic.CA2213.severity = none # Disposable fields not disposed (test containers)
+dotnet_diagnostic.CA1819.severity = none # Properties should not return arrays (test data)
+dotnet_diagnostic.CA1024.severity = none # Use properties where appropriate (test helpers)
+dotnet_diagnostic.CA2263.severity = none # Prefer generic overloads (test assertions)
+dotnet_diagnostic.CA5351.severity = suggestion # Broken cryptographic algorithms OK for test data
+dotnet_diagnostic.CA2201.severity = none # Exception type System.Exception is not sufficiently specific (test mocks)
+
+# Performance and code quality (relaxed for tests)
+dotnet_diagnostic.CA1827.severity = none # Use Any() instead of Count() (test validations)
+dotnet_diagnostic.CA1829.severity = none # Use Count property instead of Enumerable.Count (test validations)
+dotnet_diagnostic.CA1826.severity = none # Use indexable collections directly (test data)
+dotnet_diagnostic.CA1861.severity = none # Prefer static readonly fields over constant arrays (micro-optimization)
+dotnet_diagnostic.CA1063.severity = none # Implement IDisposable correctly (test infrastructure)
+dotnet_diagnostic.CA1721.severity = none # Property names confusing with methods (test mocks)
+dotnet_diagnostic.CA2214.severity = none # Do not call overridable methods in constructors (test base classes)
+dotnet_diagnostic.CA2254.severity = none # Logging message template should not vary (test logging)
+dotnet_diagnostic.CA2208.severity = none # Argument exception parameter names (test scenarios)
+dotnet_diagnostic.CA2215.severity = none # Dispose methods should call base.Dispose (test infrastructure)
+
+# xUnit specific suppressions (globally disabled to reduce noise during .NET 10 migration)
+dotnet_diagnostic.xUnit1012.severity = none # Null should not be used for type parameter (common in test data)
+# xUnit1051: Use TestContext.Current.CancellationToken - Suppressed due to:
+# - 755+ violations across test suite (introduced in xUnit v3 migration)
+# - Planned refactoring in dedicated sprint to update async tests systematically
+# - Current test harness relies on default cancellation tokens for integration test stability
+# - Re-enable after test infrastructure modernization (tracked in technical debt)
+dotnet_diagnostic.xUnit1051.severity = none
+
+# Code analysis suppressions for test code
+dotnet_diagnostic.CA2000.severity = none # Dispose objects before losing scope (false positives in test code like StringContent)
+
+# =====================================
+# IDE STYLE RULES (TODOS OS ARQUIVOS)
+# =====================================
+[*.cs]
+
+# IDE style warnings (non-critical formatting)
+dotnet_diagnostic.IDE0057.severity = suggestion # Substring can be simplified
+dotnet_diagnostic.IDE0130.severity = none # Namespace does not match folder structure (legacy projects)
+dotnet_diagnostic.IDE0010.severity = suggestion # Populate switch
+dotnet_diagnostic.IDE0040.severity = none # Accessibility modifiers required (interface members are public by default)
+dotnet_diagnostic.IDE0039.severity = none # Use local function instead of lambda (style preference)
+dotnet_diagnostic.IDE0061.severity = none # Use block body for local function (style preference)
+dotnet_diagnostic.IDE0062.severity = none # Local function can be made static (micro-optimization)
+dotnet_diagnostic.IDE0036.severity = none # Modifiers are not ordered (cosmetic)
+dotnet_diagnostic.IDE0022.severity = none # Use block body for method (endpoint style preference)
+dotnet_diagnostic.IDE0120.severity = none # Simplify LINQ expression (test scenarios)
+dotnet_diagnostic.IDE0060.severity = none # Remove unused parameter
+dotnet_diagnostic.IDE0059.severity = none # Unnecessary assignment of a value
+dotnet_diagnostic.IDE0200.severity = none # Lambda expression can be removed
+dotnet_diagnostic.IDE0290.severity = none # Use primary constructor
+dotnet_diagnostic.IDE0301.severity = none # Collection initialization can be simplified
+dotnet_diagnostic.IDE0305.severity = none # Collection initialization can be simplified
+dotnet_diagnostic.IDE0052.severity = none # Private member can be removed as the value assigned is never read
+dotnet_diagnostic.IDE0078.severity = none # Use pattern matching
+dotnet_diagnostic.IDE0005.severity = none # Using directive is unnecessary
+
+# =====================================
+# SONAR/THIRD-PARTY ANALYZER RULES
+# =====================================
+[*.cs]
+
+# SonarSource rules
+dotnet_diagnostic.S1118.severity = none # Utility classes should not have public constructors
+dotnet_diagnostic.S3903.severity = none # Types should be declared in named namespaces
+dotnet_diagnostic.S3267.severity = none # Loops should be simplified using LINQ (readability preference)
+dotnet_diagnostic.S1066.severity = none # Mergeable if statements (readability preference)
+dotnet_diagnostic.S6610.severity = none # StartsWith overloads
+dotnet_diagnostic.S6608.severity = none # Use indexing instead of LINQ Last
+dotnet_diagnostic.S3246.severity = none # Generic type parameter covariance
+dotnet_diagnostic.S2326.severity = none # Unused type parameters
+dotnet_diagnostic.S3260.severity = none # Record classes should be sealed
+dotnet_diagnostic.S4487.severity = none # Unread private fields (metrics fields)
+dotnet_diagnostic.S1135.severity = none # TODO comments
+dotnet_diagnostic.S1133.severity = none # Deprecated code comments
+dotnet_diagnostic.S1186.severity = none # Empty methods (migration methods)
+dotnet_diagnostic.S3427.severity = none # Method signature overlap
+dotnet_diagnostic.S1144.severity = none # Unused private methods
+dotnet_diagnostic.S125.severity = none # Remove commented code
+dotnet_diagnostic.S3400.severity = none # Methods that return constants
+dotnet_diagnostic.S3875.severity = none # Remove this overload of operator
+dotnet_diagnostic.S1481.severity = none # Remove the unused local variable
+dotnet_diagnostic.S1172.severity = none # Remove this unused method parameter
+dotnet_diagnostic.S1854.severity = none # Remove this useless assignment to local variable
+dotnet_diagnostic.S2139.severity = none # Either log this exception and handle it, or rethrow it
+dotnet_diagnostic.S2234.severity = none # Parameters have the same names but not the same order
+dotnet_diagnostic.S2325.severity = none # Make this method static
+dotnet_diagnostic.S2955.severity = none # Use comparison to default(T) instead
+dotnet_diagnostic.S3358.severity = none # Extract this nested ternary operation
+dotnet_diagnostic.S6667.severity = none # Logging in a catch clause should pass the caught exception
+dotnet_diagnostic.S927.severity = none # Rename parameter to match interface declaration
+
+# =====================================
+# COMPILER WARNINGS
+# =====================================
+[*.cs]
+
+# Compiler warnings (less critical in test context)
+dotnet_diagnostic.CS1570.severity = none # XML comment malformed (test documentation)
+dotnet_diagnostic.CS1998.severity = none # Async method lacks await (test methods)
+dotnet_diagnostic.CS8321.severity = none # Local function declared but never used (test helpers)
+
+# =====================================
+# NUGET E BUILD WARNINGS
+# =====================================
+[*.cs]
+
+# NuGet package warnings
+dotnet_diagnostic.NU1603.severity = none # Package dependency version conflicts
+dotnet_diagnostic.NU1605.severity = none # Package downgrade warnings
+
+# =====================================
+# ORGANIZAÇÃO E FORMATAÇÃO
+# =====================================
+[*.cs]
+
+# Organização de usings
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = false
+
+# Preferências de qualidade de código
+dotnet_analyzer_diagnostic.category-roslynator.severity = warning
+
+# =====================================
+# REGRAS ESPECÍFICAS PARA MIGRATIONS
+# =====================================
+[**/Migrations/**/*.cs]
+
+# Relaxar todas as regras em migrations (código gerado)
+dotnet_diagnostic.CA1062.severity = none
+dotnet_diagnostic.CA2000.severity = none
+dotnet_diagnostic.CA5394.severity = none
+dotnet_diagnostic.CA2100.severity = none
+dotnet_diagnostic.CA1031.severity = none
+dotnet_diagnostic.CA2007.severity = none
diff --git a/.github/workflows/aspire-ci-cd.yml b/.github/workflows/aspire-ci-cd.yml
index 4c26b2f5c..f757a7ce9 100644
--- a/.github/workflows/aspire-ci-cd.yml
+++ b/.github/workflows/aspire-ci-cd.yml
@@ -48,10 +48,10 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
- run: dotnet restore MeAjudaAi.sln --force-evaluate
+ run: dotnet restore MeAjudaAi.slnx --force-evaluate
- name: Build solution
- run: dotnet build MeAjudaAi.sln --no-restore --configuration Release --verbosity minimal
+ run: dotnet build MeAjudaAi.slnx --no-restore --configuration Release --verbosity minimal
- name: Install PostgreSQL client
run: |
@@ -133,7 +133,7 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
- run: dotnet restore MeAjudaAi.sln --force-evaluate
+ run: dotnet restore MeAjudaAi.slnx --force-evaluate
- name: Validate Aspire AppHost
run: |
@@ -168,16 +168,32 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
- run: dotnet restore MeAjudaAi.sln --force-evaluate
+ run: dotnet restore MeAjudaAi.slnx --force-evaluate
- name: Check code formatting
run: |
- dotnet format --verify-no-changes --verbosity normal \
- MeAjudaAi.sln || {
+ echo "🔍 Checking code formatting..."
+ # Only check whitespace and style (not SonarQube analyzer warnings)
+ set -o pipefail
+ dotnet format --verify-no-changes \
+ --include whitespace style \
+ --verbosity normal \
+ MeAjudaAi.slnx \
+ 2>&1 | tee format-output.txt
+ FORMAT_EXIT_CODE=${PIPESTATUS[0]}
+
+ # Check if any files were actually formatted (not just warnings)
+ if grep -q "Formatted code file" format-output.txt; then
echo "⚠️ Code formatting issues found."
- echo "Run 'dotnet format' locally to fix."
+ echo "Run 'dotnet format --include whitespace style' locally to fix."
+ grep "Formatted code file" format-output.txt
exit 1
- }
+ elif [ $FORMAT_EXIT_CODE -ne 0 ]; then
+ echo "⚠️ Formatting check failed with exit code $FORMAT_EXIT_CODE"
+ exit $FORMAT_EXIT_CODE
+ else
+ echo "✅ No formatting changes needed"
+ fi
- name: Run vulnerability scan
run: |
@@ -190,7 +206,7 @@ jobs:
echo "🔍 Running focused code quality checks..."
# Check for basic C# issues (quiet mode)
echo "Checking C# syntax..."
- dotnet build MeAjudaAi.sln --verbosity quiet --no-restore
+ dotnet build MeAjudaAi.slnx --verbosity quiet --no-restore
echo "✅ Code quality checks passed"
# Build validation for individual services (without publishing)
@@ -214,7 +230,7 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
- run: dotnet restore MeAjudaAi.sln --force-evaluate
+ run: dotnet restore MeAjudaAi.slnx --force-evaluate
- name: Validate ${{ matrix.service.name }} builds for containerization
run: |
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 8a73ed6a7..b3d913c32 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -57,10 +57,10 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: 🔧 Restore dependencies
- run: dotnet restore MeAjudaAi.sln --force-evaluate
+ run: dotnet restore MeAjudaAi.slnx --force-evaluate
- name: Build solution
- run: dotnet build MeAjudaAi.sln --configuration Release --no-restore
+ run: dotnet build MeAjudaAi.slnx --configuration Release --no-restore
- name: Setup PostgreSQL connection
id: db
diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index df71b9335..913eb8b2d 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -99,10 +99,35 @@ jobs:
sudo apt-get install -y postgresql-client
- name: 🔧 Restore dependencies
- run: dotnet restore MeAjudaAi.sln
+ run: dotnet restore MeAjudaAi.slnx
- name: Build solution
- run: dotnet build MeAjudaAi.sln --configuration Release --no-restore
+ run: dotnet build MeAjudaAi.slnx --configuration Release --no-restore
+
+ - name: Check code formatting
+ run: |
+ set -o pipefail
+ echo "🔍 Checking code formatting..."
+ # Only check whitespace and style (not SonarQube analyzer warnings)
+ dotnet format --verify-no-changes \
+ --include whitespace style \
+ --verbosity normal \
+ MeAjudaAi.slnx \
+ 2>&1 | tee format-output.txt
+ FORMAT_EXIT_CODE=${PIPESTATUS[0]}
+
+ # Check if any files were actually formatted (not just warnings)
+ if grep -q "Formatted code file" format-output.txt; then
+ echo "⚠️ Code formatting issues found."
+ echo "Run 'dotnet format --include whitespace style' locally to fix."
+ grep "Formatted code file" format-output.txt
+ exit 1
+ elif [ $FORMAT_EXIT_CODE -ne 0 ]; then
+ echo "⚠️ Formatting check failed with exit code $FORMAT_EXIT_CODE"
+ exit $FORMAT_EXIT_CODE
+ else
+ echo "✅ No formatting changes needed"
+ fi
- name: Wait for PostgreSQL to be ready
env:
@@ -929,7 +954,7 @@ jobs:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Restore dependencies
- run: dotnet restore MeAjudaAi.sln
+ run: dotnet restore MeAjudaAi.slnx
- name: Run Security Audit
run: dotnet list package --vulnerable --include-transitive
diff --git a/.github/workflows/update-api-docs.yml b/.github/workflows/update-api-docs.yml
new file mode 100644
index 000000000..32b9b4533
--- /dev/null
+++ b/.github/workflows/update-api-docs.yml
@@ -0,0 +1,188 @@
+name: 📚 Update API Documentation
+
+on:
+ push:
+ branches:
+ - main
+ - develop
+ paths:
+ # Detectar mudanças em endpoints, controllers e schemas
+ - 'src/**/API/**/*.cs'
+ - 'src/**/Controllers/**/*.cs'
+ - 'src/**/Endpoints/**/*.cs'
+ - 'src/**/DTOs/**/*.cs'
+ - 'src/**/Requests/**/*.cs'
+ - 'src/**/Responses/**/*.cs'
+ - 'src/Bootstrapper/MeAjudaAi.ApiService/**/*.cs'
+ - 'api/api-spec.json'
+ workflow_dispatch:
+ inputs:
+ force_update:
+ description: 'Forçar atualização mesmo sem mudanças'
+ required: false
+ default: 'false'
+
+permissions:
+ contents: write
+ pages: write
+ id-token: write
+
+concurrency:
+ group: api-docs-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ generate-openapi-spec:
+ name: 🔄 Gerar OpenAPI Spec
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: 📥 Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: 🔧 Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+ dotnet-quality: 'ga'
+
+ - name: 📦 Restore dependencies
+ run: dotnet restore MeAjudaAi.slnx
+
+ - name: 🔨 Build API
+ run: |
+ dotnet build src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj \
+ -c Release \
+ --no-restore \
+ -p:GenerateDocumentationFile=true
+
+ - name: 📦 Install Swashbuckle CLI
+ run: dotnet tool install -g Swashbuckle.AspNetCore.Cli --version 7.2.0
+
+ - name: 📄 Generate OpenAPI Spec
+ run: |
+ swagger tofile \
+ --output api/api-spec.json \
+ src/Bootstrapper/MeAjudaAi.ApiService/bin/Release/net10.0/MeAjudaAi.ApiService.dll \
+ v1
+
+ - name: ✅ Validate OpenAPI Spec
+ run: |
+ if [ ! -f api/api-spec.json ]; then
+ echo "❌ api-spec.json não foi gerado"
+ exit 1
+ fi
+
+ # Validar JSON
+ if ! jq empty api/api-spec.json 2>/dev/null; then
+ echo "❌ api-spec.json não é um JSON válido"
+ exit 1
+ fi
+
+ # Contar paths
+ PATH_COUNT=$(jq '.paths | length' api/api-spec.json)
+ echo "📊 Total de paths: $PATH_COUNT"
+
+ if [ "$PATH_COUNT" -lt 5 ]; then
+ echo "⚠️ Spec parece incompleto (apenas $PATH_COUNT paths)"
+ exit 1
+ fi
+
+ # Verificar AllowedCities endpoints
+ ALLOWED_CITIES_COUNT=$(jq '[.paths | keys[] | select(contains("allowed-cities"))] | length' api/api-spec.json)
+ echo "✅ Endpoints AllowedCities: $ALLOWED_CITIES_COUNT"
+
+ echo "✅ OpenAPI spec válido"
+
+ - name: 📊 Generate API Stats
+ run: |
+ echo "# 📊 API Statistics" > api-stats.md
+ echo "" >> api-stats.md
+ echo "- **Total Paths**: $(jq '.paths | length' api/api-spec.json)" >> api-stats.md
+ echo "- **Total Operations**: $(jq '[.paths[][] | select(type == "object")] | length' api/api-spec.json)" >> api-stats.md
+ echo "- **API Version**: $(jq -r '.info.version' api/api-spec.json)" >> api-stats.md
+ echo "- **Generated**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> api-stats.md
+ cat api-stats.md
+
+ - name: 💾 Commit Updated Spec
+ uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: 'docs(api): atualizar api-spec.json automaticamente [skip ci]'
+ file_pattern: 'api/api-spec.json'
+ commit_user_name: 'github-actions[bot]'
+ commit_user_email: 'github-actions[bot]@users.noreply.github.com'
+ skip_dirty_check: false
+
+ deploy-api-docs:
+ name: 🚀 Deploy API Docs to GitHub Pages
+ needs: generate-openapi-spec
+ runs-on: ubuntu-latest
+
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}api/
+
+ steps:
+ - name: 📥 Checkout
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.ref }}
+
+ - name: 📄 Create ReDoc HTML
+ run: |
+ mkdir -p docs/api
+
+ cat > docs/api/index.html << 'EOF'
+
+
+
+ MeAjudaAi API Documentation
+
+
+
+
+
+
+
+
+
+
+ EOF
+
+ echo "✅ ReDoc HTML criado"
+
+ - name: 📋 Copy OpenAPI Spec
+ run: |
+ cp api/api-spec.json docs/api/api-spec.json
+ echo "✅ OpenAPI spec copiado para docs/api/"
+
+ - name: 📦 Setup Pages
+ uses: actions/configure-pages@v4
+
+ - name: 🔨 Build with Jekyll
+ uses: actions/jekyll-build-pages@v1
+ with:
+ source: ./docs
+ destination: ./_site
+
+ - name: 📤 Upload artifact
+ uses: actions/upload-pages-artifact@v3
+
+ - name: 🚀 Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
+
+ - name: 📢 Summary
+ run: |
+ echo "## 🎉 API Documentation Deployed!" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "- 📚 **ReDoc**: ${{ steps.deployment.outputs.page_url }}api/" >> $GITHUB_STEP_SUMMARY
+ echo "- 📄 **OpenAPI Spec**: ${{ steps.deployment.outputs.page_url }}api/api-spec.json" >> $GITHUB_STEP_SUMMARY
+ echo "- 🕒 **Updated**: $(date -u +"%Y-%m-%d %H:%M:%S UTC")" >> $GITHUB_STEP_SUMMARY
diff --git a/MeAjudaAi.sln b/MeAjudaAi.sln
deleted file mode 100644
index 7f56264d3..000000000
--- a/MeAjudaAi.sln
+++ /dev/null
@@ -1,721 +0,0 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 18
-VisualStudioVersion = 18.0.11205.157
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{C43DCDF7-5D9D-4A12-928B-109444867046}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Integration.Tests", "tests\MeAjudaAi.Integration.Tests\MeAjudaAi.Integration.Tests.csproj", "{A723C0D5-0065-B6B2-3C0F-D921493AB14E}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{D5751A7E-1F4F-4D53-8623-FD882A8653B0}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bootstrapper", "Bootstrapper", "{295C5E9A-20BC-48E8-B3B9-2BC329DCD3B7}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{F98F35D2-DE8C-49BC-9785-CDFBA3EA22EF}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Shared", "src\Shared\MeAjudaAi.Shared.csproj", "{B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.ApiService", "src\Bootstrapper\MeAjudaAi.ApiService\MeAjudaAi.ApiService.csproj", "{9E8191C1-8216-B109-888B-6E4663C2CD53}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.AppHost", "src\Aspire\MeAjudaAi.AppHost\MeAjudaAi.AppHost.csproj", "{464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.ServiceDefaults", "src\Aspire\MeAjudaAi.ServiceDefaults\MeAjudaAi.ServiceDefaults.csproj", "{E2B155C0-DC26-1F78-B7E9-B4B1524A9902}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Users", "Users", "{C0DDB0A1-0AF4-4914-AB78-34E34FEFBF22}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "infrastructure", "infrastructure", "{82F49BCF-FBCA-44AD-84E5-AA53DEE4EA8E}"
- ProjectSection(SolutionItems) = preProject
- infrastructure\main.bicep = infrastructure\main.bicep
- infrastructure\servicebus.bicep = infrastructure\servicebus.bicep
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{5C93FF51-3F09-4446-9F17-146D8D91C8B8}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{8F957DC8-7EB5-4A1F-BD02-794D816D0A4C}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{DCFD7F4F-35EC-4D56-926A-AFF47A42939E}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{ACAE8CA4-04A2-4573-853B-E25B2F50671A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Users.Application", "src\Modules\Users\Application\MeAjudaAi.Modules.Users.Application.csproj", "{E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Users.Infrastructure", "src\Modules\Users\Infrastructure\MeAjudaAi.Modules.Users.Infrastructure.csproj", "{AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Users.Domain", "src\Modules\Users\Domain\MeAjudaAi.Modules.Users.Domain.csproj", "{72447551-CAC3-4135-AE06-7E8B8177229C}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Users.API", "src\Modules\Users\API\MeAjudaAi.Modules.Users.API.csproj", "{75369D09-FFEF-4213-B9EE-93733AA156F6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.E2E.Tests", "tests\MeAjudaAi.E2E.Tests\MeAjudaAi.E2E.Tests.csproj", "{D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Architecture.Tests", "tests\MeAjudaAi.Architecture.Tests\MeAjudaAi.Architecture.Tests.csproj", "{2D30D16B-DD94-4A05-9B90-AB7C56F3E545}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.ApiService.Tests", "tests\MeAjudaAi.ApiService.Tests\MeAjudaAi.ApiService.Tests.csproj", "{A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{EA1A0251-FB5A-4966-BF96-64D6F78F95AA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Users.Tests", "src\Modules\Users\Tests\MeAjudaAi.Modules.Users.Tests.csproj", "{838886D7-C244-AA56-83CC-4B20AEC7F7B6}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Providers", "Providers", "{28AE6D85-CB78-4997-151B-EEE5F92B2AA7}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{E731A8BF-B36F-2323-645D-E995216C57F6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Providers.Domain", "src\Modules\Providers\Domain\MeAjudaAi.Modules.Providers.Domain.csproj", "{C494C45E-6B0B-4BAC-859A-F233A12F685B}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{C843E946-57F7-9E6A-7E72-98B9F2022F26}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Providers.Application", "src\Modules\Providers\Application\MeAjudaAi.Modules.Providers.Application.csproj", "{0E770EAA-5962-4FFA-BA7E-C816F9336FAF}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{CD2F1C8D-651B-BD0A-D5A3-D32626A47974}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Providers.Infrastructure", "src\Modules\Providers\Infrastructure\MeAjudaAi.Modules.Providers.Infrastructure.csproj", "{96A1C31A-CE2C-4B28-A765-793CEF4F311C}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{CED78ED4-735A-6ACE-AE70-A11020087754}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Providers.API", "src\Modules\Providers\API\MeAjudaAi.Modules.Providers.API.csproj", "{902E050E-BA92-4E47-AFE3-74955FFE5B48}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C0E83F81-2ABD-45EE-8DB3-9FD92C147D80}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Providers.Tests", "src\Modules\Providers\Tests\MeAjudaAi.Modules.Providers.Tests.csproj", "{DA537C4E-E84D-A307-D5B0-81697E935BE9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Shared.Tests", "tests\MeAjudaAi.Shared.Tests\MeAjudaAi.Shared.Tests.csproj", "{1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documents", "Documents", "{AF58434C-1364-2869-CEF6-1D865BB8823F}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{74826B8F-3900-251A-9045-48B643A19426}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Documents.Domain", "src\Modules\Documents\Domain\MeAjudaAi.Modules.Documents.Domain.csproj", "{2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{1CE72D3A-35C7-8A3E-1FD4-BB368253C85D}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Documents.Application", "src\Modules\Documents\Application\MeAjudaAi.Modules.Documents.Application.csproj", "{7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{159C2A42-35C0-EED0-8201-0D2FEEEE359C}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Documents.Infrastructure", "src\Modules\Documents\Infrastructure\MeAjudaAi.Modules.Documents.Infrastructure.csproj", "{15684C16-1F84-4F59-80FA-8F5B03FA1CC3}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{FF3E0487-BA59-710F-AF19-48FE8AA634D5}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Documents.API", "src\Modules\Documents\API\MeAjudaAi.Modules.Documents.API.csproj", "{CF89B836-4146-4EC8-A1F1-4C8359133B0A}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A5D09C43-04E6-19C8-EAD9-56493F6674A3}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Documents.Tests", "src\Modules\Documents\Tests\MeAjudaAi.Modules.Documents.Tests.csproj", "{DB977F2B-C807-4C5E-BC48-64849FB6D3F8}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Locations", "Locations", "{8B641842-DDF9-E28C-3407-0C10A35A01A1}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{A297266A-1ECB-AE19-8D6F-3A458F9AD28F}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Locations.Tests", "src\Modules\Locations\Tests\MeAjudaAi.Modules.Locations.Tests.csproj", "{D0E46405-E34A-4905-BF34-9DF22036512E}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{10AEE144-453E-4C4D-B928-DBD6A8C72108}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Locations.Application", "src\Modules\Locations\Application\MeAjudaAi.Modules.Locations.Application.csproj", "{B98C73C8-F57C-747F-B86E-3A0429BFBE12}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{A7277AF8-7D65-4CE2-B6B0-2A0DB786780A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Locations.Domain", "src\Modules\Locations\Domain\MeAjudaAi.Modules.Locations.Domain.csproj", "{72B40E16-5D54-DCE4-A235-AA9F7CF99665}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{E4E48F48-72CF-41A4-AA66-9423D81C7970}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.Locations.Infrastructure", "src\Modules\Locations\Infrastructure\MeAjudaAi.Modules.Locations.Infrastructure.csproj", "{1E72996A-6FD1-9E16-5188-42DDCFF6518C}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SearchProviders", "SearchProviders", "{6FF68FBA-C4AF-48EC-AFE2-E320F2195C79}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{977BE21B-C0F0-4625-9C9D-8A5A6D8C2D49}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.SearchProviders.API", "src\Modules\SearchProviders\API\MeAjudaAi.Modules.SearchProviders.API.csproj", "{68F39D90-3AF5-9037-B03D-B08B48E4A9A8}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{6F70C0C2-B928-4F73-9D70-038F3E625A95}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.SearchProviders.Application", "src\Modules\SearchProviders\Application\MeAjudaAi.Modules.SearchProviders.Application.csproj", "{0A5B25B9-7991-B208-5D91-476CF0A14A1B}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{855F30AA-D1A2-4A1F-BB2B-68DE6D78AFEF}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.SearchProviders.Domain", "src\Modules\SearchProviders\Domain\MeAjudaAi.Modules.SearchProviders.Domain.csproj", "{C5E6E9C4-A027-5880-D304-BE3FC5E9B964}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{9BC7D786-47F5-44BB-88A1-DDEB0022FF23}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.SearchProviders.Infrastructure", "src\Modules\SearchProviders\Infrastructure\MeAjudaAi.Modules.SearchProviders.Infrastructure.csproj", "{0A64D976-2B75-C6F2-9C87-3A780C963FA3}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{4726175B-331E-49FA-A49A-EE5AC30B495A}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.SearchProviders.Tests", "src\Modules\SearchProviders\Tests\MeAjudaAi.Modules.SearchProviders.Tests.csproj", "{C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceCatalogs", "ServiceCatalogs", "{8B551008-B254-EBAF-1B6D-AB7C420234EA}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{B346CC0B-427A-E442-6F5D-8AAE1AB081D6}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Domain", "src\Modules\ServiceCatalogs\Domain\MeAjudaAi.Modules.ServiceCatalogs.Domain.csproj", "{DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{44577491-2FC0-4F52-AF5C-2BC9B323CDB7}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Application", "src\Modules\ServiceCatalogs\Application\MeAjudaAi.Modules.ServiceCatalogs.Application.csproj", "{A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infrastructure", "Infrastructure", "{8D23D6D3-2B2E-7F09-866F-FA51CC0FC081}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Infrastructure", "src\Modules\ServiceCatalogs\Infrastructure\MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.csproj", "{3B6D6C13-1E04-47B9-B44E-36D25DF913C7}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "API", "API", "{30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.API", "src\Modules\ServiceCatalogs\API\MeAjudaAi.Modules.ServiceCatalogs.API.csproj", "{B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BDD25844-1435-F5BA-1F9B-EFB3B12C916F}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MeAjudaAi.Modules.ServiceCatalogs.Tests", "src\Modules\ServiceCatalogs\Tests\MeAjudaAi.Modules.ServiceCatalogs.Tests.csproj", "{2C85E336-66A2-4B4F-845A-DBA2A6520162}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Debug|x64 = Debug|x64
- Debug|x86 = Debug|x86
- Release|Any CPU = Release|Any CPU
- Release|x64 = Release|x64
- Release|x86 = Release|x86
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Debug|x64.ActiveCfg = Debug|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Debug|x64.Build.0 = Debug|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Debug|x86.ActiveCfg = Debug|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Debug|x86.Build.0 = Debug|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Release|Any CPU.Build.0 = Release|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Release|x64.ActiveCfg = Release|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Release|x64.Build.0 = Release|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Release|x86.ActiveCfg = Release|Any CPU
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E}.Release|x86.Build.0 = Release|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Debug|x64.ActiveCfg = Debug|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Debug|x64.Build.0 = Debug|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Debug|x86.Build.0 = Debug|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Release|Any CPU.Build.0 = Release|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Release|x64.ActiveCfg = Release|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Release|x64.Build.0 = Release|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Release|x86.ActiveCfg = Release|Any CPU
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF}.Release|x86.Build.0 = Release|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Debug|x64.ActiveCfg = Debug|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Debug|x64.Build.0 = Debug|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Debug|x86.ActiveCfg = Debug|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Debug|x86.Build.0 = Debug|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Release|Any CPU.Build.0 = Release|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Release|x64.ActiveCfg = Release|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Release|x64.Build.0 = Release|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Release|x86.ActiveCfg = Release|Any CPU
- {9E8191C1-8216-B109-888B-6E4663C2CD53}.Release|x86.Build.0 = Release|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Debug|x64.ActiveCfg = Debug|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Debug|x64.Build.0 = Debug|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Debug|x86.ActiveCfg = Debug|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Debug|x86.Build.0 = Debug|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Release|Any CPU.Build.0 = Release|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Release|x64.ActiveCfg = Release|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Release|x64.Build.0 = Release|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Release|x86.ActiveCfg = Release|Any CPU
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C}.Release|x86.Build.0 = Release|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Debug|x64.ActiveCfg = Debug|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Debug|x64.Build.0 = Debug|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Debug|x86.ActiveCfg = Debug|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Debug|x86.Build.0 = Debug|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Release|Any CPU.Build.0 = Release|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Release|x64.ActiveCfg = Release|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Release|x64.Build.0 = Release|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Release|x86.ActiveCfg = Release|Any CPU
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902}.Release|x86.Build.0 = Release|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Debug|x64.ActiveCfg = Debug|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Debug|x64.Build.0 = Debug|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Debug|x86.ActiveCfg = Debug|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Debug|x86.Build.0 = Debug|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Release|Any CPU.Build.0 = Release|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Release|x64.ActiveCfg = Release|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Release|x64.Build.0 = Release|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Release|x86.ActiveCfg = Release|Any CPU
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8}.Release|x86.Build.0 = Release|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Debug|x64.ActiveCfg = Debug|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Debug|x64.Build.0 = Debug|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Debug|x86.ActiveCfg = Debug|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Debug|x86.Build.0 = Debug|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Release|Any CPU.Build.0 = Release|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Release|x64.ActiveCfg = Release|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Release|x64.Build.0 = Release|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Release|x86.ActiveCfg = Release|Any CPU
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D}.Release|x86.Build.0 = Release|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Debug|x64.ActiveCfg = Debug|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Debug|x64.Build.0 = Debug|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Debug|x86.ActiveCfg = Debug|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Debug|x86.Build.0 = Debug|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Release|Any CPU.Build.0 = Release|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Release|x64.ActiveCfg = Release|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Release|x64.Build.0 = Release|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Release|x86.ActiveCfg = Release|Any CPU
- {72447551-CAC3-4135-AE06-7E8B8177229C}.Release|x86.Build.0 = Release|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Debug|x64.ActiveCfg = Debug|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Debug|x64.Build.0 = Debug|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Debug|x86.ActiveCfg = Debug|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Debug|x86.Build.0 = Debug|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Release|Any CPU.Build.0 = Release|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Release|x64.ActiveCfg = Release|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Release|x64.Build.0 = Release|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Release|x86.ActiveCfg = Release|Any CPU
- {75369D09-FFEF-4213-B9EE-93733AA156F6}.Release|x86.Build.0 = Release|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Debug|x64.Build.0 = Debug|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Debug|x86.Build.0 = Debug|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Release|Any CPU.Build.0 = Release|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Release|x64.ActiveCfg = Release|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Release|x64.Build.0 = Release|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Release|x86.ActiveCfg = Release|Any CPU
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A}.Release|x86.Build.0 = Release|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Debug|x64.ActiveCfg = Debug|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Debug|x64.Build.0 = Debug|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Debug|x86.ActiveCfg = Debug|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Debug|x86.Build.0 = Debug|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Release|Any CPU.Build.0 = Release|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Release|x64.ActiveCfg = Release|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Release|x64.Build.0 = Release|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Release|x86.ActiveCfg = Release|Any CPU
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545}.Release|x86.Build.0 = Release|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Debug|x64.ActiveCfg = Debug|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Debug|x64.Build.0 = Debug|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Debug|x86.ActiveCfg = Debug|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Debug|x86.Build.0 = Debug|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Release|Any CPU.Build.0 = Release|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Release|x64.ActiveCfg = Release|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Release|x64.Build.0 = Release|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Release|x86.ActiveCfg = Release|Any CPU
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6}.Release|x86.Build.0 = Release|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Debug|x64.ActiveCfg = Debug|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Debug|x64.Build.0 = Debug|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Debug|x86.ActiveCfg = Debug|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Debug|x86.Build.0 = Debug|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Release|Any CPU.Build.0 = Release|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Release|x64.ActiveCfg = Release|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Release|x64.Build.0 = Release|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Release|x86.ActiveCfg = Release|Any CPU
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6}.Release|x86.Build.0 = Release|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Debug|x64.Build.0 = Debug|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Debug|x86.Build.0 = Debug|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Release|Any CPU.Build.0 = Release|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Release|x64.ActiveCfg = Release|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Release|x64.Build.0 = Release|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Release|x86.ActiveCfg = Release|Any CPU
- {C494C45E-6B0B-4BAC-859A-F233A12F685B}.Release|x86.Build.0 = Release|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Debug|x64.ActiveCfg = Debug|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Debug|x64.Build.0 = Debug|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Debug|x86.ActiveCfg = Debug|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Debug|x86.Build.0 = Debug|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Release|Any CPU.Build.0 = Release|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Release|x64.ActiveCfg = Release|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Release|x64.Build.0 = Release|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Release|x86.ActiveCfg = Release|Any CPU
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF}.Release|x86.Build.0 = Release|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Debug|x64.ActiveCfg = Debug|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Debug|x64.Build.0 = Debug|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Debug|x86.ActiveCfg = Debug|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Debug|x86.Build.0 = Debug|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Release|Any CPU.Build.0 = Release|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Release|x64.ActiveCfg = Release|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Release|x64.Build.0 = Release|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Release|x86.ActiveCfg = Release|Any CPU
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C}.Release|x86.Build.0 = Release|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Debug|x64.ActiveCfg = Debug|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Debug|x64.Build.0 = Debug|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Debug|x86.ActiveCfg = Debug|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Debug|x86.Build.0 = Debug|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Release|Any CPU.Build.0 = Release|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Release|x64.ActiveCfg = Release|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Release|x64.Build.0 = Release|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Release|x86.ActiveCfg = Release|Any CPU
- {902E050E-BA92-4E47-AFE3-74955FFE5B48}.Release|x86.Build.0 = Release|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Debug|x64.ActiveCfg = Debug|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Debug|x64.Build.0 = Debug|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Debug|x86.ActiveCfg = Debug|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Debug|x86.Build.0 = Debug|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Release|Any CPU.Build.0 = Release|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Release|x64.ActiveCfg = Release|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Release|x64.Build.0 = Release|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Release|x86.ActiveCfg = Release|Any CPU
- {DA537C4E-E84D-A307-D5B0-81697E935BE9}.Release|x86.Build.0 = Release|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Debug|x64.ActiveCfg = Debug|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Debug|x64.Build.0 = Debug|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Debug|x86.ActiveCfg = Debug|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Debug|x86.Build.0 = Debug|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Release|Any CPU.Build.0 = Release|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Release|x64.ActiveCfg = Release|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Release|x64.Build.0 = Release|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Release|x86.ActiveCfg = Release|Any CPU
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3}.Release|x86.Build.0 = Release|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Debug|x64.ActiveCfg = Debug|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Debug|x64.Build.0 = Debug|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Debug|x86.ActiveCfg = Debug|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Debug|x86.Build.0 = Debug|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Release|Any CPU.Build.0 = Release|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Release|x64.ActiveCfg = Release|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Release|x64.Build.0 = Release|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Release|x86.ActiveCfg = Release|Any CPU
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3}.Release|x86.Build.0 = Release|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Debug|x64.ActiveCfg = Debug|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Debug|x64.Build.0 = Debug|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Debug|x86.ActiveCfg = Debug|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Debug|x86.Build.0 = Debug|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Release|Any CPU.Build.0 = Release|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Release|x64.ActiveCfg = Release|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Release|x64.Build.0 = Release|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Release|x86.ActiveCfg = Release|Any CPU
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0}.Release|x86.Build.0 = Release|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Debug|x64.ActiveCfg = Debug|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Debug|x64.Build.0 = Debug|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Debug|x86.ActiveCfg = Debug|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Debug|x86.Build.0 = Debug|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Release|Any CPU.Build.0 = Release|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Release|x64.ActiveCfg = Release|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Release|x64.Build.0 = Release|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Release|x86.ActiveCfg = Release|Any CPU
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3}.Release|x86.Build.0 = Release|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Debug|x64.Build.0 = Debug|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Debug|x86.Build.0 = Debug|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Release|Any CPU.Build.0 = Release|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Release|x64.ActiveCfg = Release|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Release|x64.Build.0 = Release|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Release|x86.ActiveCfg = Release|Any CPU
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A}.Release|x86.Build.0 = Release|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Debug|x64.ActiveCfg = Debug|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Debug|x64.Build.0 = Debug|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Debug|x86.ActiveCfg = Debug|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Debug|x86.Build.0 = Debug|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Release|Any CPU.Build.0 = Release|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Release|x64.ActiveCfg = Release|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Release|x64.Build.0 = Release|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Release|x86.ActiveCfg = Release|Any CPU
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8}.Release|x86.Build.0 = Release|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Debug|x64.ActiveCfg = Debug|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Debug|x64.Build.0 = Debug|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Debug|x86.ActiveCfg = Debug|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Debug|x86.Build.0 = Debug|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Release|Any CPU.Build.0 = Release|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Release|x64.ActiveCfg = Release|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Release|x64.Build.0 = Release|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Release|x86.ActiveCfg = Release|Any CPU
- {D0E46405-E34A-4905-BF34-9DF22036512E}.Release|x86.Build.0 = Release|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Debug|x64.ActiveCfg = Debug|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Debug|x64.Build.0 = Debug|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Debug|x86.Build.0 = Debug|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Release|Any CPU.Build.0 = Release|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Release|x64.ActiveCfg = Release|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Release|x64.Build.0 = Release|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Release|x86.ActiveCfg = Release|Any CPU
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12}.Release|x86.Build.0 = Release|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Debug|x64.ActiveCfg = Debug|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Debug|x64.Build.0 = Debug|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Debug|x86.ActiveCfg = Debug|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Debug|x86.Build.0 = Debug|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Release|Any CPU.Build.0 = Release|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Release|x64.ActiveCfg = Release|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Release|x64.Build.0 = Release|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Release|x86.ActiveCfg = Release|Any CPU
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665}.Release|x86.Build.0 = Release|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Debug|x64.ActiveCfg = Debug|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Debug|x64.Build.0 = Debug|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Debug|x86.ActiveCfg = Debug|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Debug|x86.Build.0 = Debug|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Release|Any CPU.Build.0 = Release|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Release|x64.ActiveCfg = Release|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Release|x64.Build.0 = Release|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Release|x86.ActiveCfg = Release|Any CPU
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C}.Release|x86.Build.0 = Release|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Debug|x64.ActiveCfg = Debug|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Debug|x64.Build.0 = Debug|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Debug|x86.ActiveCfg = Debug|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Debug|x86.Build.0 = Debug|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Release|Any CPU.Build.0 = Release|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Release|x64.ActiveCfg = Release|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Release|x64.Build.0 = Release|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Release|x86.ActiveCfg = Release|Any CPU
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8}.Release|x86.Build.0 = Release|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Debug|x64.ActiveCfg = Debug|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Debug|x64.Build.0 = Debug|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Debug|x86.ActiveCfg = Debug|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Debug|x86.Build.0 = Debug|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Release|Any CPU.Build.0 = Release|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Release|x64.ActiveCfg = Release|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Release|x64.Build.0 = Release|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Release|x86.ActiveCfg = Release|Any CPU
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B}.Release|x86.Build.0 = Release|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Debug|x64.Build.0 = Debug|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Debug|x86.Build.0 = Debug|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Release|Any CPU.Build.0 = Release|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Release|x64.ActiveCfg = Release|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Release|x64.Build.0 = Release|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Release|x86.ActiveCfg = Release|Any CPU
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964}.Release|x86.Build.0 = Release|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Debug|x64.ActiveCfg = Debug|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Debug|x64.Build.0 = Debug|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Debug|x86.ActiveCfg = Debug|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Debug|x86.Build.0 = Debug|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Release|Any CPU.Build.0 = Release|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Release|x64.ActiveCfg = Release|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Release|x64.Build.0 = Release|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Release|x86.ActiveCfg = Release|Any CPU
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3}.Release|x86.Build.0 = Release|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Debug|x64.ActiveCfg = Debug|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Debug|x64.Build.0 = Debug|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Debug|x86.ActiveCfg = Debug|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Debug|x86.Build.0 = Debug|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Release|Any CPU.Build.0 = Release|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Release|x64.ActiveCfg = Release|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Release|x64.Build.0 = Release|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Release|x86.ActiveCfg = Release|Any CPU
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B}.Release|x86.Build.0 = Release|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Debug|x64.Build.0 = Debug|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Debug|x86.Build.0 = Debug|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|Any CPU.Build.0 = Release|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|x64.ActiveCfg = Release|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|x64.Build.0 = Release|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|x86.ActiveCfg = Release|Any CPU
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A}.Release|x86.Build.0 = Release|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x64.ActiveCfg = Debug|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x64.Build.0 = Debug|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x86.ActiveCfg = Debug|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Debug|x86.Build.0 = Debug|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|Any CPU.Build.0 = Release|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x64.ActiveCfg = Release|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x64.Build.0 = Release|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x86.ActiveCfg = Release|Any CPU
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D}.Release|x86.Build.0 = Release|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|x64.Build.0 = Debug|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Debug|x86.Build.0 = Debug|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|Any CPU.Build.0 = Release|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x64.ActiveCfg = Release|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x64.Build.0 = Release|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x86.ActiveCfg = Release|Any CPU
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7}.Release|x86.Build.0 = Release|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x64.ActiveCfg = Debug|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x64.Build.0 = Debug|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Debug|x86.Build.0 = Debug|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|Any CPU.Build.0 = Release|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x64.ActiveCfg = Release|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x64.Build.0 = Release|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x86.ActiveCfg = Release|Any CPU
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E}.Release|x86.Build.0 = Release|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x64.ActiveCfg = Debug|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x64.Build.0 = Debug|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x86.ActiveCfg = Debug|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Debug|x86.Build.0 = Debug|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|Any CPU.Build.0 = Release|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x64.ActiveCfg = Release|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x64.Build.0 = Release|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x86.ActiveCfg = Release|Any CPU
- {2C85E336-66A2-4B4F-845A-DBA2A6520162}.Release|x86.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {A723C0D5-0065-B6B2-3C0F-D921493AB14E} = {C43DCDF7-5D9D-4A12-928B-109444867046}
- {D5751A7E-1F4F-4D53-8623-FD882A8653B0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
- {295C5E9A-20BC-48E8-B3B9-2BC329DCD3B7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
- {D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
- {F98F35D2-DE8C-49BC-9785-CDFBA3EA22EF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
- {B5465160-1EE9-9113-4E66-4FCF2EC8F5DF} = {F98F35D2-DE8C-49BC-9785-CDFBA3EA22EF}
- {9E8191C1-8216-B109-888B-6E4663C2CD53} = {295C5E9A-20BC-48E8-B3B9-2BC329DCD3B7}
- {464E05CB-AA53-B1B2-CC71-6ABB65F2C62C} = {D5751A7E-1F4F-4D53-8623-FD882A8653B0}
- {E2B155C0-DC26-1F78-B7E9-B4B1524A9902} = {D5751A7E-1F4F-4D53-8623-FD882A8653B0}
- {C0DDB0A1-0AF4-4914-AB78-34E34FEFBF22} = {D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F}
- {5C93FF51-3F09-4446-9F17-146D8D91C8B8} = {C0DDB0A1-0AF4-4914-AB78-34E34FEFBF22}
- {8F957DC8-7EB5-4A1F-BD02-794D816D0A4C} = {C0DDB0A1-0AF4-4914-AB78-34E34FEFBF22}
- {DCFD7F4F-35EC-4D56-926A-AFF47A42939E} = {C0DDB0A1-0AF4-4914-AB78-34E34FEFBF22}
- {ACAE8CA4-04A2-4573-853B-E25B2F50671A} = {C0DDB0A1-0AF4-4914-AB78-34E34FEFBF22}
- {E891CA21-7F1E-4A35-AF98-9D2A5C0104D8} = {5C93FF51-3F09-4446-9F17-146D8D91C8B8}
- {AEC75B4E-7D10-4FCF-BEB8-E04A4A3AE29D} = {8F957DC8-7EB5-4A1F-BD02-794D816D0A4C}
- {72447551-CAC3-4135-AE06-7E8B8177229C} = {DCFD7F4F-35EC-4D56-926A-AFF47A42939E}
- {75369D09-FFEF-4213-B9EE-93733AA156F6} = {ACAE8CA4-04A2-4573-853B-E25B2F50671A}
- {D50E8B11-B918-4CFA-90B8-D8B60A0DDE7A} = {C43DCDF7-5D9D-4A12-928B-109444867046}
- {2D30D16B-DD94-4A05-9B90-AB7C56F3E545} = {C43DCDF7-5D9D-4A12-928B-109444867046}
- {A2B3C4D5-E6F7-4A5B-9C8D-E1F2A3B4C5D6} = {C43DCDF7-5D9D-4A12-928B-109444867046}
- {EA1A0251-FB5A-4966-BF96-64D6F78F95AA} = {C0DDB0A1-0AF4-4914-AB78-34E34FEFBF22}
- {838886D7-C244-AA56-83CC-4B20AEC7F7B6} = {EA1A0251-FB5A-4966-BF96-64D6F78F95AA}
- {28AE6D85-CB78-4997-151B-EEE5F92B2AA7} = {D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F}
- {E731A8BF-B36F-2323-645D-E995216C57F6} = {28AE6D85-CB78-4997-151B-EEE5F92B2AA7}
- {C494C45E-6B0B-4BAC-859A-F233A12F685B} = {E731A8BF-B36F-2323-645D-E995216C57F6}
- {C843E946-57F7-9E6A-7E72-98B9F2022F26} = {28AE6D85-CB78-4997-151B-EEE5F92B2AA7}
- {0E770EAA-5962-4FFA-BA7E-C816F9336FAF} = {C843E946-57F7-9E6A-7E72-98B9F2022F26}
- {CD2F1C8D-651B-BD0A-D5A3-D32626A47974} = {28AE6D85-CB78-4997-151B-EEE5F92B2AA7}
- {96A1C31A-CE2C-4B28-A765-793CEF4F311C} = {CD2F1C8D-651B-BD0A-D5A3-D32626A47974}
- {CED78ED4-735A-6ACE-AE70-A11020087754} = {28AE6D85-CB78-4997-151B-EEE5F92B2AA7}
- {902E050E-BA92-4E47-AFE3-74955FFE5B48} = {CED78ED4-735A-6ACE-AE70-A11020087754}
- {C0E83F81-2ABD-45EE-8DB3-9FD92C147D80} = {28AE6D85-CB78-4997-151B-EEE5F92B2AA7}
- {DA537C4E-E84D-A307-D5B0-81697E935BE9} = {C0E83F81-2ABD-45EE-8DB3-9FD92C147D80}
- {1C58A0BA-2B84-488A-BA7F-E16351F6B2B3} = {C43DCDF7-5D9D-4A12-928B-109444867046}
- {AF58434C-1364-2869-CEF6-1D865BB8823F} = {D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F}
- {74826B8F-3900-251A-9045-48B643A19426} = {AF58434C-1364-2869-CEF6-1D865BB8823F}
- {2B7BA03C-DA0F-4BD3-825D-3794D608C7D3} = {74826B8F-3900-251A-9045-48B643A19426}
- {1CE72D3A-35C7-8A3E-1FD4-BB368253C85D} = {AF58434C-1364-2869-CEF6-1D865BB8823F}
- {7422EE1F-9173-4F3F-BB5D-1DC8ACFA5EF0} = {1CE72D3A-35C7-8A3E-1FD4-BB368253C85D}
- {159C2A42-35C0-EED0-8201-0D2FEEEE359C} = {AF58434C-1364-2869-CEF6-1D865BB8823F}
- {15684C16-1F84-4F59-80FA-8F5B03FA1CC3} = {159C2A42-35C0-EED0-8201-0D2FEEEE359C}
- {FF3E0487-BA59-710F-AF19-48FE8AA634D5} = {AF58434C-1364-2869-CEF6-1D865BB8823F}
- {CF89B836-4146-4EC8-A1F1-4C8359133B0A} = {FF3E0487-BA59-710F-AF19-48FE8AA634D5}
- {A5D09C43-04E6-19C8-EAD9-56493F6674A3} = {AF58434C-1364-2869-CEF6-1D865BB8823F}
- {DB977F2B-C807-4C5E-BC48-64849FB6D3F8} = {A5D09C43-04E6-19C8-EAD9-56493F6674A3}
- {8B641842-DDF9-E28C-3407-0C10A35A01A1} = {D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F}
- {A297266A-1ECB-AE19-8D6F-3A458F9AD28F} = {8B641842-DDF9-E28C-3407-0C10A35A01A1}
- {D0E46405-E34A-4905-BF34-9DF22036512E} = {A297266A-1ECB-AE19-8D6F-3A458F9AD28F}
- {10AEE144-453E-4C4D-B928-DBD6A8C72108} = {8B641842-DDF9-E28C-3407-0C10A35A01A1}
- {B98C73C8-F57C-747F-B86E-3A0429BFBE12} = {10AEE144-453E-4C4D-B928-DBD6A8C72108}
- {A7277AF8-7D65-4CE2-B6B0-2A0DB786780A} = {8B641842-DDF9-E28C-3407-0C10A35A01A1}
- {72B40E16-5D54-DCE4-A235-AA9F7CF99665} = {A7277AF8-7D65-4CE2-B6B0-2A0DB786780A}
- {E4E48F48-72CF-41A4-AA66-9423D81C7970} = {8B641842-DDF9-E28C-3407-0C10A35A01A1}
- {1E72996A-6FD1-9E16-5188-42DDCFF6518C} = {E4E48F48-72CF-41A4-AA66-9423D81C7970}
- {6FF68FBA-C4AF-48EC-AFE2-E320F2195C79} = {D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F}
- {977BE21B-C0F0-4625-9C9D-8A5A6D8C2D49} = {6FF68FBA-C4AF-48EC-AFE2-E320F2195C79}
- {68F39D90-3AF5-9037-B03D-B08B48E4A9A8} = {977BE21B-C0F0-4625-9C9D-8A5A6D8C2D49}
- {6F70C0C2-B928-4F73-9D70-038F3E625A95} = {6FF68FBA-C4AF-48EC-AFE2-E320F2195C79}
- {0A5B25B9-7991-B208-5D91-476CF0A14A1B} = {6F70C0C2-B928-4F73-9D70-038F3E625A95}
- {855F30AA-D1A2-4A1F-BB2B-68DE6D78AFEF} = {6FF68FBA-C4AF-48EC-AFE2-E320F2195C79}
- {C5E6E9C4-A027-5880-D304-BE3FC5E9B964} = {855F30AA-D1A2-4A1F-BB2B-68DE6D78AFEF}
- {9BC7D786-47F5-44BB-88A1-DDEB0022FF23} = {6FF68FBA-C4AF-48EC-AFE2-E320F2195C79}
- {0A64D976-2B75-C6F2-9C87-3A780C963FA3} = {9BC7D786-47F5-44BB-88A1-DDEB0022FF23}
- {4726175B-331E-49FA-A49A-EE5AC30B495A} = {6FF68FBA-C4AF-48EC-AFE2-E320F2195C79}
- {C7F6B6F4-4F9C-C844-500C-87E3802A6C4B} = {4726175B-331E-49FA-A49A-EE5AC30B495A}
- {8B551008-B254-EBAF-1B6D-AB7C420234EA} = {D55DFAF4-45A1-4C45-AA54-8CE46F0AFB1F}
- {B346CC0B-427A-E442-6F5D-8AAE1AB081D6} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
- {DC1D1ACD-A21E-4BA0-A22D-77450234BD2A} = {B346CC0B-427A-E442-6F5D-8AAE1AB081D6}
- {44577491-2FC0-4F52-AF5C-2BC9B323CDB7} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
- {A7B8C9D0-1E2F-3A4B-5C6D-7E8F9A0B1C2D} = {44577491-2FC0-4F52-AF5C-2BC9B323CDB7}
- {8D23D6D3-2B2E-7F09-866F-FA51CC0FC081} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
- {3B6D6C13-1E04-47B9-B44E-36D25DF913C7} = {8D23D6D3-2B2E-7F09-866F-FA51CC0FC081}
- {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
- {B1C2D3E4-F5A6-7B8C-9D0E-1F2A3B4C5D6E} = {30A2D3C4-AF98-40A1-AA90-ED7C5FE090F8}
- {BDD25844-1435-F5BA-1F9B-EFB3B12C916F} = {8B551008-B254-EBAF-1B6D-AB7C420234EA}
- {2C85E336-66A2-4B4F-845A-DBA2A6520162} = {BDD25844-1435-F5BA-1F9B-EFB3B12C916F}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {391B5342-8EC5-4DF0-BCDA-6D73F87E8751}
- EndGlobalSection
-EndGlobal
diff --git a/MeAjudaAi.slnx b/MeAjudaAi.slnx
new file mode 100644
index 000000000..263eee34d
--- /dev/null
+++ b/MeAjudaAi.slnx
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index b8331535f..0ad908733 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ O **MeAjudaAi** é uma plataforma moderna de marketplace de serviços que implem
- **Docker** - Containerização
- **Azure** - Hospedagem em nuvem
-## � Documentação
+## 📚 Documentação
A documentação completa do projeto está disponível em **MkDocs Material** com suporte completo em português.
@@ -110,35 +110,30 @@ O projeto foi organizado para facilitar navegação e manutenção:
Para instruções detalhadas, consulte o [**Guia de Desenvolvimento Completo**](./docs/development.md).
-**Setup completo (recomendado):**
-```bash
-./run-local.sh setup
-```
-
-**Execução rápida:**
-```bash
-./run-local.sh run
+**Setup via .NET Aspire:**
+```powershell
+# Execute o AppHost do Aspire
+cd src/Aspire/MeAjudaAi.AppHost
+dotnet run
```
-**Modo interativo:**
-```bash
-./run-local.sh
+**Ou via Docker Compose:**
+```powershell
+cd infrastructure/compose
+docker compose -f environments/development.yml up -d
```
### Para Testes
-```bash
+```powershell
# Todos os testes
-./test.sh all
-
-# Apenas unitários
-./test.sh unit
+dotnet test
# Com relatório de cobertura
-./test.sh coverage
+dotnet test --collect:"XPlat Code Coverage"
```
-📖 **[Guia Completo de Desenvolvimento](docs/development_guide.md)**
+📖 **[Guia Completo de Desenvolvimento](docs/development.md)**
### Pré-requisitos
@@ -197,6 +192,8 @@ docker compose -f environments/development.yml up -d
> - **API Service**: `src/Bootstrapper/MeAjudaAi.ApiService/Properties/launchSettings.json`
> - **Infraestrutura**: `infrastructure/compose/environments/development.yml`
+> ⚠️ **Somente desenvolvimento**: credenciais/portas abaixo são valores locais de exemplo. Não reutilize em produção.
+
| Serviço | URL | Credenciais |
|---------|-----|-------------|
| **Aspire Dashboard** | [https://localhost:17063](https://localhost:17063)
[http://localhost:15297](http://localhost:15297) | - |
@@ -460,7 +457,7 @@ azd provision
- [**Guia de Infraestrutura**](docs/infrastructure.md) - Setup e deploy
- [**Arquitetura e Padrões**](docs/architecture.md) - Decisões arquiteturais
-- [**Guia de Desenvolvimento**](docs/development_guide.md) - Convenções e práticas
+- [**Guia de Desenvolvimento**](docs/development.md) - Convenções e práticas
- [**CI/CD**](docs/ci-cd.md) - Pipeline de integração contínua
- [**Diretrizes de Desenvolvimento**](docs/development-guidelines.md) - Padrões e boas práticas
diff --git a/api/README.md b/api/README.md
index 2bd6cd750..6276a129b 100644
--- a/api/README.md
+++ b/api/README.md
@@ -19,16 +19,60 @@ Complete OpenAPI specification containing:
## Generation
-The API specification is automatically generated using the export script:
+⚠️ **IMPORTANTE**: O arquivo `api-spec.json` deve ser atualizado sempre que houver mudanças nos endpoints da API.
-```bash
-# Generate current API specification
-./scripts/export-openapi.ps1
+### Quando Atualizar
+Atualize o arquivo após:
+- ✅ Adicionar novos endpoints
+- ✅ Modificar schemas de request/response
+- ✅ Alterar rotas ou métodos HTTP
+- ✅ Modificar validações ou DTOs
+- ✅ Atualizar documentação XML dos endpoints
+
+### Como Atualizar
-# Generate to custom location
-./scripts/export-openapi.ps1 -OutputPath "api/my-api-spec.json"
+```bash
+# Windows: Gerar OpenAPI spec + Postman Collections
+cd tools/api-collections
+.\generate-all-collections.bat
+
+# Linux/macOS: Gerar OpenAPI spec + Postman Collections
+cd tools/api-collections
+./generate-all-collections.sh
+
+# Apenas OpenAPI (sem Collections)
+cd tools/api-collections
+npm install
+node generate-postman-collections.js
+
+# Após gerar, commitar as mudanças
+git add api/api-spec.json
+git commit -m "docs: atualizar especificação OpenAPI"
```
+### Automação (GitHub Pages)
+
+#### 🤖 Geração Automática
+O `api-spec.json` é **automaticamente atualizado** via GitHub Actions sempre que houver mudanças em:
+- Controllers, endpoints, DTOs
+- Requests, Responses, schemas
+- Qualquer arquivo em `src/**/API/`
+
+**Workflow**: `.github/workflows/update-api-docs.yml`
+
+#### 🔄 Processo Automatizado
+1. ✅ Detecta mudanças em endpoints (via `paths` no workflow)
+2. 🔨 Builda a aplicação (Release mode)
+3. 📄 Gera `api-spec.json` via Swashbuckle CLI
+4. ✅ Valida JSON e conta endpoints
+5. 💾 Commita automaticamente (com `[skip ci]`)
+6. 🚀 Faz deploy para GitHub Pages com ReDoc
+
+#### 📚 URLs Publicadas
+- 📖 **ReDoc (interativo)**: [ReDoc Interface](https://frigini.github.io/MeAjudaAi/api/)
+- 📄 **OpenAPI JSON**: [OpenAPI Specification](https://frigini.github.io/MeAjudaAi/api/api-spec.json)
+- 🔄 **Atualização**: Automática a cada push na branch `main`
+
## Features
### Offline Generation
diff --git a/docs/api-automation.md b/docs/api-automation.md
new file mode 100644
index 000000000..0a7e0deee
--- /dev/null
+++ b/docs/api-automation.md
@@ -0,0 +1,325 @@
+# 🤖 Automação de Documentação da API
+
+## 📋 Visão Geral
+
+Sistema de automação completo para manter a documentação da API **sempre atualizada** sem intervenção manual.
+
+## 🎯 Objetivo
+
+Garantir que o `api-spec.json` e a documentação no GitHub Pages reflitam **sempre** o estado atual dos endpoints da API.
+
+## ⚙️ Como Funciona
+
+### 1. Detecção de Mudanças
+
+O workflow GitHub Actions (`.github/workflows/update-api-docs.yml`) é acionado quando há commits em:
+
+```yaml
+paths:
+ - 'src/**/API/**/*.cs' # Controllers e endpoints
+ - 'src/**/Controllers/**/*.cs' # Controllers
+ - 'src/**/Endpoints/**/*.cs' # Minimal APIs
+ - 'src/**/DTOs/**/*.cs' # Data Transfer Objects
+ - 'src/**/Requests/**/*.cs' # Request models
+ - 'src/**/Responses/**/*.cs' # Response models
+ - 'src/Bootstrapper/MeAjudaAi.ApiService/**/*.cs'
+```
+
+**Exemplos de mudanças detectadas:**
+- ✅ Novo endpoint criado
+- ✅ Schema de request/response alterado
+- ✅ Rota modificada
+- ✅ Validações adicionadas
+- ✅ Documentação XML atualizada
+
+### 2. Geração do OpenAPI Spec
+
+```bash
+# Build da aplicação
+dotnet build -c Release
+
+# Instalação do Swashbuckle CLI
+dotnet tool install -g Swashbuckle.AspNetCore.Cli
+
+# Extração do OpenAPI spec
+swagger tofile --output api/api-spec.json \
+ src/Bootstrapper/MeAjudaAi.ApiService/bin/Release/net10.0/MeAjudaAi.ApiService.dll \
+ v1
+```
+
+**Vantagens:**
+- ✅ Não precisa rodar a API (sem PostgreSQL, Keycloak, etc.)
+- ✅ Usa assemblies compiladas
+- ✅ Reflete exatamente o código atual
+
+### 3. Validação
+
+O workflow valida o spec gerado:
+
+```bash
+# Validar JSON
+jq empty api/api-spec.json
+
+# Contar endpoints
+PATH_COUNT=$(jq '.paths | length' api/api-spec.json)
+
+# Validar mínimo de paths
+if [ "$PATH_COUNT" -lt 5 ]; then
+ echo "⚠️ Spec incompleto"
+ exit 1
+fi
+```
+
+### 4. Commit Automático
+
+```yaml
+- uses: stefanzweifel/git-auto-commit-action@v5
+ with:
+ commit_message: 'docs(api): atualizar api-spec.json automaticamente [skip ci]'
+ file_pattern: 'api/api-spec.json'
+```
+
+**Nota:** `[skip ci]` evita loop infinito de builds.
+
+### 5. Deploy para GitHub Pages
+
+```yaml
+- name: Create ReDoc HTML
+ # Cria docs/api/index.html com ReDoc
+
+- name: Deploy to GitHub Pages
+ uses: actions/deploy-pages@v4
+```
+
+## 🔄 Fluxo Completo
+
+```mermaid
+graph TD
+ A[Developer altera endpoint] --> B[Commit & Push]
+ B --> C{Paths alterados?}
+ C -->|Sim| D[GitHub Actions trigger]
+ C -->|Não| E[Workflow não executa]
+ D --> F[Build aplicação]
+ F --> G[Gerar api-spec.json]
+ G --> H[Validar JSON]
+ H --> I{Válido?}
+ I -->|Sim| J[Commit api-spec.json]
+ I -->|Não| K[Falha no workflow]
+ J --> L[Deploy ReDoc para Pages]
+ L --> M[Documentação atualizada]
+```
+
+## 📚 URLs Publicadas
+
+### GitHub Pages
+- **ReDoc (navegável)**: https://frigini.github.io/MeAjudaAi/api/
+- **OpenAPI JSON**: https://frigini.github.io/MeAjudaAi/api/api-spec.json
+
+### Swagger UI (local)
+- **Swagger UI**: http://localhost:5000/swagger
+- **OpenAPI JSON**: http://localhost:5000/api-docs/v1/swagger.json
+
+## 🛠️ Uso Local (Desenvolvimento)
+
+### Opção 1: Script Batch/Shell (gera tudo)
+
+```bash
+# Windows
+cd tools/api-collections
+.\generate-all-collections.bat
+
+# Linux/macOS
+cd tools/api-collections
+./generate-all-collections.sh
+```
+
+**O que faz:**
+- ✅ Builda a aplicação
+- ✅ Inicia API em background
+- ✅ Aguarda API ficar pronta
+- ✅ Gera `api-spec.json`
+- ✅ Gera Postman Collections
+- ✅ Cria Environments (dev/staging/prod)
+- ✅ Para a API
+
+### Opção 2: Node.js apenas (só spec + collections)
+
+```bash
+# Pré-requisito: API rodando
+cd src/Bootstrapper/MeAjudaAi.ApiService
+dotnet run
+
+# Terminal 2: Gerar
+cd tools/api-collections
+npm install
+node generate-postman-collections.js
+```
+
+**Vantagens:**
+- ✅ Gera api-spec.json
+- ✅ Gera Postman Collections
+- ✅ Cria environments (dev/staging/prod)
+- ✅ Testes automáticos incluídos
+
+## 🔧 Configuração Inicial
+
+### 1. Habilitar GitHub Pages
+
+No repositório GitHub:
+1. **Settings** → **Pages**
+2. **Source**: GitHub Actions
+3. **Branch**: main
+4. Salvar
+
+### 2. Permissões do Workflow
+
+Garantir que o workflow tenha permissões:
+
+```yaml
+permissions:
+ contents: write # Commit do api-spec.json
+ pages: write # Deploy para Pages
+ id-token: write # Autenticação
+```
+
+### 3. Primeira Execução
+
+```bash
+# Fazer qualquer mudança em endpoint
+git add .
+git commit -m "feat: adicionar novo endpoint"
+git push origin main
+
+# Acompanhar em: Actions → Update API Documentation
+```
+
+## 📊 Estatísticas
+
+O workflow gera estatísticas automáticas:
+
+```markdown
+# 📊 API Statistics
+- **Total Paths**: 42
+- **Total Operations**: 87
+- **API Version**: 1.0.0
+- **Generated**: 2024-12-12 13:45:00 UTC
+```
+
+## ⚠️ Importante
+
+### Quando o Spec É Atualizado
+
+**SIM - Atualização automática:**
+- ✅ Novo endpoint criado
+- ✅ Rota modificada
+- ✅ Schema de request/response alterado
+- ✅ Validações adicionadas/removidas
+- ✅ Documentação XML atualizada
+- ✅ Parâmetros de query/path modificados
+
+**NÃO - Sem atualização:**
+- ❌ Mudanças em lógica de negócio
+- ❌ Alterações em repositórios
+- ❌ Mudanças em services internos
+- ❌ Configurações de appsettings.json
+
+### Evitar Loops Infinitos
+
+O commit automático usa `[skip ci]` para não acionar outro workflow:
+
+```yaml
+commit_message: 'docs(api): atualizar api-spec.json automaticamente [skip ci]'
+```
+
+## 🧪 Testes
+
+### Testar Localmente
+
+```bash
+# Gerar spec + collections localmente
+cd tools/api-collections
+.\generate-all-collections.bat # Windows
+./generate-all-collections.sh # Linux/macOS
+
+# Verificar se spec foi gerado
+ls -la ../../api/api-spec.json
+
+# Validar JSON
+cat ../../api/api-spec.json | jq '.'
+```
+
+### Testar ReDoc Localmente
+
+```bash
+# Servir docs localmente
+cd docs
+python -m http.server 8000
+
+# Abrir no navegador
+# http://localhost:8000/api/
+```
+
+## 🎉 Benefícios
+
+### Para Desenvolvedores
+- ✅ Documentação sempre atualizada
+- ✅ Zero esforço manual
+- ✅ Commits focados em features, não em docs
+
+### Para Frontend
+- ✅ Specs sempre refletem backend atual
+- ✅ Importação fácil em clients (Postman, Insomnia)
+- ✅ TypeScript types podem ser gerados do spec
+
+### Para QA
+- ✅ Collections Postman atualizadas
+- ✅ Testes sempre alinhados com endpoints
+- ✅ Documentação de schemas completa
+
+### Para DevOps
+- ✅ CI/CD integrado
+- ✅ Validação automática
+- ✅ Deploy sem intervenção
+
+## 📝 Troubleshooting
+
+### Workflow falhou
+
+**Problema:** Build failed
+```bash
+# Verificar localmente
+dotnet build src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj -c Release
+```
+
+**Problema:** Spec inválido
+```bash
+# Validar JSON
+jq empty api/api-spec.json
+```
+
+**Problema:** Poucas paths detectadas
+```bash
+# Verificar se endpoints têm XML docs e atributos corretos
+# Verificar se [ApiController] e [Route] estão presentes
+```
+
+### GitHub Pages não atualizou
+
+**Solução:**
+1. Verificar em **Actions** se deploy ocorreu
+2. Aguardar ~5 minutos (cache do GitHub Pages)
+3. Force refresh no navegador (Ctrl+Shift+R)
+4. Verificar se **Settings** → **Pages** está habilitado
+
+## 🔗 Links Úteis
+
+- [Swashbuckle Documentation](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)
+- [ReDoc Documentation](https://github.com/Redocly/redoc)
+- [GitHub Actions - Pages](https://github.com/actions/deploy-pages)
+- [OpenAPI Specification](https://swagger.io/specification/)
+
+---
+
+**Última atualização:** 12/12/2024
+**Workflow:** `.github/workflows/update-api-docs.yml`
+**Responsável:** DevOps Team
diff --git a/docs/architecture.md b/docs/architecture.md
index 6770ca4cc..846eb7ee8 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -64,6 +64,656 @@ src/
└── MeAjudaAi.ServiceDefaults/ # Configurações padrão
```
+---
+
+## 🎨 Design Patterns Implementados
+
+Este projeto implementa diversos padrões de design consolidados para garantir manutenibilidade, testabilidade e escalabilidade.
+
+### 1. **Repository Pattern**
+
+**Propósito**: Abstrair acesso a dados, permitindo testes unitários e troca de implementação.
+
+**Implementação Real**:
+
+```csharp
+// Interface do repositório (Domain Layer)
+public interface IAllowedCityRepository
+{
+ Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
+ Task GetByCityAndStateAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
+ Task ExistsAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
+ Task IsCityAllowedAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
+ Task AddAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
+ Task UpdateAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
+ Task DeleteAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
+}
+
+// Implementação EF Core (Infrastructure Layer)
+internal sealed class AllowedCityRepository(LocationsDbContext context) : IAllowedCityRepository
+{
+ public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
+ {
+ return await context.AllowedCities
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
+ }
+
+ public async Task IsCityAllowedAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default)
+ {
+ var normalizedCity = cityName?.Trim() ?? string.Empty;
+ var normalizedState = stateSigla?.Trim().ToUpperInvariant() ?? string.Empty;
+
+ return await context.AllowedCities
+ .AnyAsync(x =>
+ EF.Functions.ILike(x.CityName, normalizedCity) &&
+ x.StateSigla == normalizedState &&
+ x.IsActive,
+ cancellationToken);
+ }
+}
+```
+
+**Benefícios**:
+- ✅ Testes unitários sem banco de dados (mocks)
+- ✅ Encapsulamento de queries complexas
+- ✅ Possibilidade de cache transparente
+
+---
+
+### 2. **CQRS (Command Query Responsibility Segregation)**
+
+**Propósito**: Separar operações de leitura (queries) das de escrita (commands).
+
+**Implementação Real - Command**:
+
+```csharp
+// Command (Application Layer)
+public sealed record CreateAllowedCityCommand(
+ string CityName,
+ string StateSigla,
+ string? IbgeCode = null
+) : ICommand>;
+
+// Handler (Application Layer)
+internal sealed class CreateAllowedCityCommandHandler(
+ IAllowedCityRepository repository,
+ IUnitOfWork unitOfWork,
+ ILogger logger)
+ : ICommandHandler>
+{
+ public async Task> HandleAsync(
+ CreateAllowedCityCommand command,
+ CancellationToken cancellationToken)
+ {
+ // 1. Validar duplicação
+ if (await repository.ExistsAsync(command.CityName, command.StateSigla, cancellationToken))
+ {
+ return Result.Failure(LocationsErrors.CityAlreadyExists(command.CityName, command.StateSigla));
+ }
+
+ // 2. Criar entidade de domínio
+ var allowedCity = AllowedCity.Create(
+ command.CityName,
+ command.StateSigla,
+ command.IbgeCode
+ );
+
+ // 3. Persistir
+ await repository.AddAsync(allowedCity, cancellationToken);
+ await unitOfWork.CommitAsync(cancellationToken);
+
+ logger.LogInformation("Cidade permitida criada: {CityName}/{State}", command.CityName, command.StateSigla);
+
+ return Result.Success(allowedCity.Id);
+ }
+}
+```
+
+**Implementação Real - Query**:
+
+```csharp
+// Query (Application Layer)
+public sealed record GetServiceCategoryByIdQuery(Guid CategoryId) : IQuery>;
+
+// Handler (Application Layer)
+internal sealed class GetServiceCategoryByIdQueryHandler(
+ IServiceCategoryRepository repository)
+ : IQueryHandler>
+{
+ public async Task> HandleAsync(
+ GetServiceCategoryByIdQuery query,
+ CancellationToken cancellationToken)
+ {
+ var category = await repository.GetByIdAsync(query.CategoryId, cancellationToken);
+
+ if (category is null)
+ {
+ return Result.Success(null);
+ }
+
+ var dto = ServiceCategoryMapper.ToDto(category);
+ return Result.Success(dto);
+ }
+}
+```
+
+**Benefícios**:
+- ✅ Separação clara de responsabilidades
+- ✅ Otimização independente de leitura vs escrita
+- ✅ Testabilidade individual de cada operação
+- ✅ Escalabilidade (queries podem usar read replicas)
+
+---
+
+### 3. **Domain Events**
+
+**Propósito**: Comunicação desacoplada entre agregados e módulos.
+
+**Implementação Real**:
+
+```csharp
+// Evento de Domínio
+public sealed record ProviderRegisteredDomainEvent(
+ Guid ProviderId,
+ Guid UserId,
+ string Name,
+ EProviderType Type
+) : IDomainEvent
+{
+ public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
+}
+
+// Handler do Evento (Infrastructure Layer)
+internal sealed class ProviderRegisteredDomainEventHandler(
+ IMessageBus messageBus,
+ ILogger logger)
+ : IDomainEventHandler
+{
+ public async Task Handle(ProviderRegisteredDomainEvent notification, CancellationToken cancellationToken)
+ {
+ try
+ {
+ // Publicar evento de integração para outros módulos
+ var integrationEvent = new ProviderRegisteredIntegrationEvent(
+ notification.ProviderId,
+ notification.UserId,
+ notification.Name,
+ notification.Type.ToString()
+ );
+
+ await messageBus.PublishAsync(integrationEvent, cancellationToken);
+
+ logger.LogInformation(
+ "Evento de integração publicado para Provider {ProviderId}",
+ notification.ProviderId);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Erro ao processar evento ProviderRegisteredDomainEvent");
+ throw;
+ }
+ }
+}
+
+// Uso no Agregado
+public class Provider : AggregateRoot
+{
+ public static Provider Create(Guid userId, string name, EProviderType type, /* ... */)
+ {
+ var provider = new Provider
+ {
+ Id = UuidGenerator.NewId(),
+ UserId = userId,
+ Name = name,
+ Type = type,
+ // ...
+ };
+
+ // Adicionar evento de domínio
+ provider.AddDomainEvent(new ProviderRegisteredDomainEvent(
+ provider.Id,
+ userId,
+ name,
+ type
+ ));
+
+ return provider;
+ }
+}
+```
+
+**Benefícios**:
+- ✅ Desacoplamento entre agregados
+- ✅ Auditoria automática de mudanças
+- ✅ Integração assíncrona entre módulos
+- ✅ Extensibilidade (novos handlers sem alterar código existente)
+
+---
+
+### 4. **Unit of Work Pattern**
+
+**Propósito**: Coordenar mudanças em múltiplos repositórios com transações.
+
+**Implementação Real**:
+
+```csharp
+// Interface (Shared Layer)
+public interface IUnitOfWork
+{
+ Task CommitAsync(CancellationToken cancellationToken = default);
+ Task RollbackAsync(CancellationToken cancellationToken = default);
+}
+
+// Implementação EF Core (Infrastructure Layer)
+internal sealed class UnitOfWork(DbContext context) : IUnitOfWork
+{
+ public async Task CommitAsync(CancellationToken cancellationToken = default)
+ {
+ // EF Core já gerencia transação implicitamente
+ return await context.SaveChangesAsync(cancellationToken);
+ }
+
+ public async Task RollbackAsync(CancellationToken cancellationToken = default)
+ {
+ await context.Database.RollbackTransactionAsync(cancellationToken);
+ }
+}
+
+// Uso em Handler
+internal sealed class UpdateProviderProfileCommandHandler(
+ IProviderRepository providerRepository,
+ IDocumentsModuleApi documentsApi,
+ IUnitOfWork unitOfWork)
+{
+ public async Task HandleAsync(UpdateProviderProfileCommand command, CancellationToken ct)
+ {
+ // 1. Buscar provider
+ var provider = await providerRepository.GetByIdAsync(command.ProviderId, ct);
+
+ // 2. Atualizar aggregate
+ provider.UpdateProfile(/* ... */);
+
+ // 3. Atualizar no repositório
+ await providerRepository.UpdateAsync(provider, ct);
+
+ // 4. Commit atômico (transação)
+ await unitOfWork.CommitAsync(ct);
+
+ return Result.Success();
+ }
+}
+```
+
+**Benefícios**:
+- ✅ Transações atômicas
+- ✅ Coordenação de múltiplas mudanças
+- ✅ Rollback automático em caso de erro
+
+---
+
+### 5. **Factory Pattern**
+
+**Propósito**: Encapsular lógica de criação de objetos complexos.
+
+**Implementação Real**:
+
+```csharp
+// UuidGenerator Factory (Shared/Time)
+public static class UuidGenerator
+{
+ public static Guid NewId()
+ {
+ return Guid.CreateVersion7(); // UUID v7 com timestamp ordenável
+ }
+}
+
+// SerilogConfigurator Factory (Shared/Logging)
+public static class SerilogConfigurator
+{
+ public static ILogger CreateLogger(IConfiguration configuration, string environmentName)
+ {
+ var loggerConfig = new LoggerConfiguration()
+ .ReadFrom.Configuration(configuration)
+ .Enrich.WithProperty("Application", "MeAjudaAi")
+ .Enrich.WithProperty("Environment", environmentName)
+ .Enrich.WithMachineName()
+ .Enrich.WithThreadId();
+
+ if (environmentName == "Development")
+ {
+ loggerConfig.WriteTo.Console();
+ }
+
+ loggerConfig.WriteTo.File(
+ "logs/app-.log",
+ rollingInterval: RollingInterval.Day,
+ retainedFileCountLimit: 7
+ );
+
+ return loggerConfig.CreateLogger();
+ }
+}
+```
+
+**Benefícios**:
+- ✅ Encapsulamento de lógica de criação
+- ✅ Configuração centralizada
+- ✅ Fácil substituição de implementação
+
+---
+
+### 6. **Strategy Pattern**
+
+**Propósito**: Selecionar algoritmo/implementação em runtime.
+
+**Implementação Real** (MessageBus):
+
+```csharp
+// Interface comum (Shared/Messaging)
+public interface IMessageBus
+{
+ Task PublishAsync(T message, CancellationToken cancellationToken = default);
+ Task SubscribeAsync(Func handler, CancellationToken cancellationToken = default);
+}
+
+// Estratégia 1: RabbitMQ
+public class RabbitMqMessageBus : IMessageBus
+{
+ public async Task PublishAsync(T message, CancellationToken ct)
+ {
+ // Implementação RabbitMQ
+ }
+}
+
+// Estratégia 2: Azure Service Bus
+public class ServiceBusMessageBus : IMessageBus
+{
+ public async Task PublishAsync(T message, CancellationToken ct)
+ {
+ // Implementação Azure Service Bus
+ }
+}
+
+// Seleção em runtime (Program.cs)
+var messageBusProvider = builder.Configuration["MessageBus:Provider"];
+
+if (messageBusProvider == "ServiceBus")
+{
+ builder.Services.AddSingleton();
+}
+else
+{
+ builder.Services.AddSingleton();
+}
+```
+
+**Benefícios**:
+- ✅ Troca de implementação sem alterar código cliente
+- ✅ Suporte a múltiplos providers (RabbitMQ, Azure, Kafka)
+- ✅ Testabilidade (mocks)
+
+---
+
+### 7. **Decorator Pattern** (via Pipeline Behaviors)
+
+**Propósito**: Adicionar comportamentos cross-cutting (logging, validação, cache) transparentemente.
+
+**Implementação Real**:
+
+```csharp
+// Behavior para Caching (Shared/Behaviors)
+public class CachingBehavior(
+ ICacheService cacheService,
+ ILogger> logger)
+ : IPipelineBehavior
+ where TRequest : IRequest
+{
+ public async Task Handle(
+ TRequest request,
+ RequestHandlerDelegate next,
+ CancellationToken cancellationToken)
+ {
+ // Só aplica cache se query implementa ICacheableQuery
+ if (request is not ICacheableQuery cacheableQuery)
+ {
+ return await next();
+ }
+
+ var cacheKey = cacheableQuery.GetCacheKey();
+ var cacheExpiration = cacheableQuery.GetCacheExpiration();
+
+ // Tentar buscar no cache
+ var (cachedResult, isCached) = await cacheService.GetAsync(cacheKey, cancellationToken);
+ if (isCached)
+ {
+ logger.LogDebug("Cache hit for key: {CacheKey}", cacheKey);
+ return cachedResult;
+ }
+
+ // Executar query e cachear resultado
+ var result = await next();
+
+ if (result is not null)
+ {
+ await cacheService.SetAsync(cacheKey, result, cacheExpiration, cancellationToken);
+ }
+
+ return result;
+ }
+}
+
+// Registro (Application Layer Extensions)
+services.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachingBehavior<,>));
+services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
+services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
+```
+
+**Benefícios**:
+- ✅ Concerns cross-cutting sem poluir handlers
+- ✅ Ordem de execução configurável
+- ✅ Adição/remoção de behaviors sem alterar código
+
+---
+
+### 8. **Options Pattern**
+
+**Propósito**: Configuração fortemente tipada via injeção de dependência.
+
+**Implementação Real**:
+
+```csharp
+// Opções fortemente tipadas (Shared/Messaging)
+public sealed class MessageBusOptions
+{
+ public const string SectionName = "MessageBus";
+
+ public string Provider { get; set; } = "RabbitMQ"; // ou "ServiceBus"
+ public string ConnectionString { get; set; } = string.Empty;
+ public int RetryCount { get; set; } = 3;
+ public int RetryDelaySeconds { get; set; } = 5;
+}
+
+// Registro no Program.cs
+builder.Services.Configure(
+ builder.Configuration.GetSection(MessageBusOptions.SectionName));
+
+// Uso via injeção
+public class RabbitMqMessageBus(
+ IOptions options,
+ ILogger logger)
+{
+ private readonly MessageBusOptions _options = options.Value;
+
+ public async Task PublishAsync(T message, CancellationToken ct)
+ {
+ // Usa _options.ConnectionString, _options.RetryCount, etc.
+ }
+}
+```
+
+**Benefícios**:
+- ✅ Configuração fortemente tipada (compile-time safety)
+- ✅ Validação via Data Annotations
+- ✅ Hot reload de configurações (IOptionsSnapshot)
+
+---
+
+### 9. **Middleware Pipeline Pattern**
+
+**Propósito**: Processar requisições HTTP em cadeia com responsabilidades isoladas.
+
+**Implementação Real**:
+
+```csharp
+// Middleware customizado (ApiService/Middlewares)
+public class GeographicRestrictionMiddleware(
+ RequestDelegate next,
+ ILocationsModuleApi locationsApi,
+ ILogger logger)
+{
+ public async Task InvokeAsync(HttpContext context)
+ {
+ // 1. Verificar se endpoint requer restrição geográfica
+ var endpoint = context.GetEndpoint();
+ var restrictionAttribute = endpoint?.Metadata
+ .GetMetadata();
+
+ if (restrictionAttribute is null)
+ {
+ await next(context);
+ return;
+ }
+
+ // 2. Extrair cidade/estado da requisição
+ var city = context.Request.Headers["X-City"].ToString();
+ var state = context.Request.Headers["X-State"].ToString();
+
+ if (string.IsNullOrEmpty(city) || string.IsNullOrEmpty(state))
+ {
+ context.Response.StatusCode = 400;
+ await context.Response.WriteAsJsonAsync(new { error = "City and State required" });
+ return;
+ }
+
+ // 3. Validar via LocationsModuleApi
+ var isAllowed = await locationsApi.IsCityAllowedAsync(city, state);
+
+ if (!isAllowed.IsSuccess || !isAllowed.Value)
+ {
+ context.Response.StatusCode = 403;
+ await context.Response.WriteAsJsonAsync(new { error = "City not allowed" });
+ return;
+ }
+
+ // 4. Continuar pipeline
+ await next(context);
+ }
+}
+
+// Registro no pipeline (Program.cs)
+app.UseMiddleware();
+```
+
+**Benefícios**:
+- ✅ Separação de concerns (logging, auth, validação)
+- ✅ Ordem de execução clara
+- ✅ Reutilização entre endpoints
+
+---
+
+## 🚫 Anti-Patterns Evitados
+
+### ❌ **Anemic Domain Model**
+**Evitado**: Entidades ricas com comportamento encapsulado.
+
+```csharp
+// ❌ ANTI-PATTERN: Anemic Domain
+public class Provider
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ public string Status { get; set; } // string sem validação
+}
+
+// ✅ PATTERN CORRETO: Rich Domain Model
+public class Provider : AggregateRoot
+{
+ public string Name { get; private set; }
+ public EProviderStatus Status { get; private set; }
+
+ public void Activate(string adminEmail)
+ {
+ if (Status != EProviderStatus.PendingApproval)
+ throw new InvalidOperationException("Provider must be pending approval");
+
+ Status = EProviderStatus.Active;
+ AddDomainEvent(new ProviderActivatedDomainEvent(Id, adminEmail));
+ }
+}
+```
+
+### ❌ **Repository Anti-Patterns**
+**Evitado**: Repositórios genéricos com métodos desnecessários.
+
+```csharp
+// ❌ ANTI-PATTERN: Generic Repository com métodos inutilizados
+public interface IRepository
+{
+ Task GetByIdAsync(Guid id);
+ Task> GetAllAsync(); // Perigoso: pode retornar milhões de registros
+ Task AddAsync(T entity);
+ Task UpdateAsync(T entity);
+ Task DeleteAsync(T entity);
+}
+
+// ✅ PATTERN CORRETO: Repositórios específicos por agregado
+public interface IProviderRepository
+{
+ Task GetByIdAsync(Guid id, CancellationToken ct);
+ Task GetByUserIdAsync(Guid userId, CancellationToken ct);
+ Task> GetByCityAsync(string city, int pageSize, int page, CancellationToken ct);
+ // Apenas métodos realmente necessários
+}
+```
+
+### ❌ **Service Locator**
+**Evitado**: Dependency Injection explícita via construtor.
+
+```csharp
+// ❌ ANTI-PATTERN: Service Locator
+public class ProviderService
+{
+ public void RegisterProvider(RegisterProviderDto dto)
+ {
+ var repository = ServiceLocator.GetService();
+ var logger = ServiceLocator.GetService();
+ // Dependências ocultas, difícil de testar
+ }
+}
+
+// ✅ PATTERN CORRETO: Constructor Injection
+public class RegisterProviderCommandHandler(
+ IProviderRepository repository,
+ IUnitOfWork unitOfWork,
+ ILogger logger)
+{
+ // Dependências explícitas e testáveis
+}
+```
+
+---
+
+## 📚 Referências e Boas Práticas
+
+- **Clean Architecture**: Uncle Bob (Robert C. Martin)
+- **Domain-Driven Design**: Eric Evans, Vaughn Vernon
+- **CQRS**: Greg Young, Udi Dahan
+- **Modular Monolith**: Milan Jovanovic, Kamil Grzybek
+- **Repository Pattern**: Martin Fowler
+- **.NET Design Patterns**: Microsoft Docs
+
+---
+
## 🎯 Domain-Driven Design (DDD)
### **Bounded Contexts**
@@ -1512,21 +2162,13 @@ src/Shared/API.Collections/Generated/
#### **Filtros Personalizados**
-```
-// Exemplos automáticos baseados em convenções
-options.SchemaFilter();
-
-// Tags organizadas por módulos
-options.DocumentFilter();
-
+```csharp
// Versionamento de API
options.OperationFilter();
-`sql
+```
#### **Melhorias Implementadas**
-- **📝 Exemplos Inteligentes**: Baseados em nomes de propriedades e tipos
-- **🏷️ Tags Organizadas**: Agrupamento lógico por módulos
- **🔒 Segurança JWT**: Configuração automática de Bearer tokens
- **📊 Schemas Reutilizáveis**: Componentes comuns (paginação, erros)
- **🌍 Multi-ambiente**: URLs para dev/staging/production
diff --git a/docs/configuration-templates/configure-environment.sh b/docs/configuration-templates/configure-environment.sh
deleted file mode 100644
index b06e0b2a2..000000000
--- a/docs/configuration-templates/configure-environment.sh
+++ /dev/null
@@ -1,201 +0,0 @@
-#!/bin/bash
-
-# Script de configuração automatizada para diferentes ambientes
-# Uso: ./configure-environment.sh [development|production]
-
-set -e
-
-ENVIRONMENT=${1:-development}
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
-CONFIG_DIR="$PROJECT_ROOT/docs/configuration-templates"
-TARGET_DIR="$PROJECT_ROOT/src/Bootstrapper/MeAjudaAi.ApiService"
-
-echo "🔧 Configurando ambiente: $ENVIRONMENT"
-
-# Validar ambiente
-case $ENVIRONMENT in
- development|production)
- ;;
- *)
- echo "❌ Ambiente inválido: $ENVIRONMENT"
- echo "Ambientes suportados: development, production"
- exit 1
- ;;
-esac
-
-# Função para copiar e configurar arquivo
-configure_appsettings() {
- local env=$1
- local template_file="$CONFIG_DIR/appsettings.$env.template.json"
- local target_file="$TARGET_DIR/appsettings.$env.json"
-
- if [ ! -f "$template_file" ]; then
- echo "❌ Template não encontrado: $template_file"
- exit 1
- fi
-
- echo "📄 Copiando template para: $target_file"
- cp "$template_file" "$target_file"
-
- # Substituir variáveis de ambiente se estiverem definidas
- if [[ "${env,,}" != "development" ]]; then
- echo "🔄 Substituindo variáveis de ambiente..."
-
- # Lista de variáveis esperadas
- declare -a vars=(
- "DATABASE_CONNECTION_STRING"
- "REDIS_CONNECTION_STRING"
- "KEYCLOAK_BASE_URL"
- "KEYCLOAK_CLIENT_ID"
- "KEYCLOAK_CLIENT_SECRET"
- "SERVICEBUS_CONNECTION_STRING"
- "RABBITMQ_HOSTNAME"
- "RABBITMQ_USERNAME"
- "RABBITMQ_PASSWORD"
- )
-
- for var in "${vars[@]}"; do
- if [ ! -z "${!var}" ]; then
- echo " ✅ Substituindo \${$var}"
- sed -i "s|\${$var}|${!var}|g" "$target_file"
- else
- echo " ⚠️ Variável não definida: $var"
- fi
- done
- fi
-
- echo "✅ Configuração criada: $target_file"
-}
-
-# Função para validar configuração
-validate_config() {
- local env=$1
- local config_file="$TARGET_DIR/appsettings.$env.json"
-
- echo "🔍 Validando configuração..."
-
- if ! command -v jq &> /dev/null; then
- echo "⚠️ jq não encontrado - validação JSON ignorada"
- return 0
- fi
-
- if ! jq empty "$config_file" 2>/dev/null; then
- echo "❌ JSON inválido em: $config_file"
- exit 1
- fi
-
- # Validações específicas por ambiente
- case "${env,,}" in
- production)
- # Verificar se ainda há variáveis não substituídas
- if grep -q '\${' "$config_file"; then
- echo "❌ Variáveis não substituídas encontradas em produção:"
- grep '\${' "$config_file"
- exit 1
- fi
-
- # Verificar configurações de segurança
- if ! jq -e '.Security.EnforceHttps == true' "$config_file" >/dev/null; then
- echo "❌ HTTPS deve estar habilitado em produção"
- exit 1
- fi
- ;;
- esac
-
- echo "✅ Configuração válida"
-}
-
-# Função para criar arquivo de ambiente
-create_env_file() {
- local env=$1
- local env_file="$PROJECT_ROOT/.env.$env"
-
- if [[ "${env,,}" = "development" ]]; then
- echo "⏭️ Arquivo .env não necessário para development"
- return 0
- fi
-
- echo "📝 Criando arquivo de exemplo: $env_file.example"
-
- cat > "$env_file.example" << EOF
-# Variáveis de ambiente para $env
-# Copie este arquivo para .env.$env e configure os valores reais
-
-# Database
-DATABASE_CONNECTION_STRING="Host=your-db-host;Database=meajudaai_$env;Username=your-user;Password=your-password;Port=5432;SslMode=Require;"
-
-# Redis
-REDIS_CONNECTION_STRING="your-redis-host:6379"
-
-# Keycloak
-KEYCLOAK_BASE_URL="https://your-keycloak-host"
-KEYCLOAK_CLIENT_ID="meajudaai-$env"
-KEYCLOAK_CLIENT_SECRET="your-keycloak-secret"
-
-# Messaging
-SERVICEBUS_CONNECTION_STRING="your-servicebus-connection"
-RABBITMQ_HOSTNAME="your-rabbitmq-host"
-RABBITMQ_USERNAME="your-rabbitmq-user"
-RABBITMQ_PASSWORD="your-rabbitmq-password"
-EOF
-
- echo "✅ Arquivo de exemplo criado: $env_file.example"
-}
-
-# Função principal
-main() {
- echo "🚀 Iniciando configuração do ambiente $ENVIRONMENT"
-
- # Criar diretório de destino se não existir
- mkdir -p "$TARGET_DIR"
-
- # Configurar appsettings
- case $ENVIRONMENT in
- development)
- configure_appsettings "Development"
- ;;
- production)
- configure_appsettings "Production"
- ;;
- esac
-
- # Validar configuração
- case $ENVIRONMENT in
- development)
- validate_config "Development"
- ;;
- production)
- validate_config "Production"
- ;;
- esac
-
- # Criar arquivo de ambiente
- create_env_file "$ENVIRONMENT"
-
- echo ""
- echo "🎉 Configuração do ambiente $ENVIRONMENT concluída!"
- echo ""
- echo "📋 Próximos passos:"
-
- case $ENVIRONMENT in
- development)
- echo " 1. Execute: dotnet run --project $TARGET_DIR"
- echo " 2. Acesse: http://localhost:5000/swagger"
- ;;
- production)
- echo " 1. Configure as variáveis de ambiente em .env.$ENVIRONMENT"
- echo " 2. Configure o serviço de secrets (Azure Key Vault, etc.)"
- echo " 3. Execute o deploy para $ENVIRONMENT"
- echo " 4. Verifique os health checks"
- ;;
- esac
-
- echo ""
- echo "📚 Documentação: $CONFIG_DIR/README.md"
-}
-
-# Verificar se está sendo executado como script principal
-if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
- main "$@"
-fi
\ No newline at end of file
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 74078f422..a92fb5e83 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -7,7 +7,7 @@ Este documento consolida o planejamento estratégico e tático da plataforma MeA
## 📊 Sumário Executivo
**Projeto**: MeAjudaAi - Plataforma de Conexão entre Clientes e Prestadores de Serviços
-**Status Geral**: Fase 1 ✅ | Sprint 0 ✅ (21 Nov) | Sprint 1 ✅ (2 Dez) | Sprint 2 ✅ (10 Dez) | Sprint 3 🔄 (BRANCH CRIADA 10 Dez) | MVP Target: 31/Março/2025
+**Status Geral**: Fase 1 ✅ | Sprint 0 ✅ (21 Nov) | Sprint 1 ✅ (2 Dez) | Sprint 2 ✅ (10 Dez) | Sprint 3-P1 ✅ (11 Dez) | Sprint 3-P2 ✅ (13 Dez - CONCLUÍDO!) | MVP Target: 31/Março/2026
**Cobertura de Testes**: 28.2% → **90.56% ALCANÇADO** (Sprint 2 - META SUPERADA EM 55.56pp!)
**Stack**: .NET 10 LTS + Aspire 13 + PostgreSQL + Blazor WASM + MAUI Hybrid
@@ -16,7 +16,8 @@ Este documento consolida o planejamento estratégico e tático da plataforma MeA
- ✅ **Jan 20 - 21 Nov**: Sprint 0 - Migration .NET 10 + Aspire 13 (CONCLUÍDO e MERGED)
- ✅ **22 Nov - 2 Dez**: Sprint 1 - Geographic Restriction + Module Integration (CONCLUÍDO e MERGED)
- ✅ **3 Dez - 10 Dez**: Sprint 2 - Test Coverage 90.56% (CONCLUÍDO - META 35% SUPERADA!)
-- 🔄 **10 Dez - 24 Dez**: Sprint 3 - GitHub Pages Documentation (EM ANDAMENTO - branch criada)
+- ✅ **10 Dez - 11 Dez**: Sprint 3 Parte 1 - GitHub Pages Migration (CONCLUÍDO - DEPLOYED!)
+- 🔄 **11 Dez - 24 Dez**: Sprint 3 Parte 2 - Admin Endpoints (EM ANDAMENTO - branch criada)
- ⏳ **Dezembro 2025-Janeiro 2026**: Sprints 4-5 - Frontend Blazor (Web)
- ⏳ **Fevereiro-Março 2026**: Sprints 6-7 - Frontend Blazor (Web + Mobile)
- 🎯 **31 de Março de 2026**: MVP Launch (Admin Portal + Customer App)
@@ -35,7 +36,23 @@ Fundação técnica para escalabilidade e produção:
- ✅ Migration .NET 10 + Aspire 13 (Sprint 0 - CONCLUÍDO 21 Nov, MERGED to master)
- ✅ Geographic Restriction + Module Integration (Sprint 1 - CONCLUÍDO 2 Dez, MERGED to master)
- ✅ Test Coverage 90.56% (Sprint 2 - CONCLUÍDO 10 Dez - META 35% SUPERADA EM 55.56pp!)
-- 🔄 GitHub Pages Documentation Migration (Sprint 3 - EM ANDAMENTO desde 10 Dez)
+- ✅ GitHub Pages Documentation Migration (Sprint 3 Parte 1 - CONCLUÍDO 11 Dez - DEPLOYED!)
+
+**✅ Sprint 3 Parte 2: CONCLUÍDA** (11 Dez - 13 Dez 2025)
+Admin Endpoints & Tools - TODAS AS PARTES FINALIZADAS:
+- ✅ Admin: Endpoints CRUD para gerenciar cidades permitidas (COMPLETO)
+ - ✅ Banco de dados: LocationsDbContext + migrations
+ - ✅ Domínio: AllowedCity entity + IAllowedCityRepository
+ - ✅ Handlers: CRUD completo (5 handlers)
+ - ✅ Endpoints: GET/POST/PUT/DELETE configurados
+ - ✅ Exception Handling: Domain exceptions + IExceptionHandler (404/400 corretos)
+ - ✅ Testes: 4 integration + 15 E2E (100% passando)
+ - ✅ Quality: 0 warnings, dotnet format executado
+- ✅ Tools: Bruno Collections para todos módulos (35+ arquivos .bru)
+- ✅ Scripts: Auditoria completa e documentação (commit b0b94707)
+- ✅ Module Integrations: Providers ↔ ServiceCatalogs + Locations
+- ✅ Code Quality: NSubstitute→Moq, UuidGenerator, .slnx, SonarQube warnings
+- ✅ CI/CD: Formatting checks corrigidos, exit code masking resolvido
**⏳ Fase 2: PLANEJADO** (Fevereiro–Março 2026)
Frontend Blazor WASM + MAUI Hybrid:
@@ -59,14 +76,15 @@ A implementação segue os princípios arquiteturais definidos em `architecture.
---
-## 📅 Cronograma de Sprints (Janeiro-Março 2025)
+## 📅 Cronograma de Sprints (Novembro 2025-Março 2026)
| Sprint | Duração | Período | Objetivo | Status |
|--------|---------|---------|----------|--------|
| **Sprint 0** | 4 semanas | Jan 20 - 21 Nov | Migration .NET 10 + Aspire 13 | ✅ CONCLUÍDO (21 Nov - MERGED) |
| **Sprint 1** | 10 dias | 22 Nov - 2 Dez | Geographic Restriction + Module Integration | ✅ CONCLUÍDO (2 Dez - MERGED) |
| **Sprint 2** | 1 semana | 3 Dez - 10 Dez | Test Coverage 90.56% | ✅ CONCLUÍDO (10 Dez - META SUPERADA!) |
-| **Sprint 3** | 2 semanas | 10 Dez - 24 Dez | GitHub Pages Documentation | 🔄 EM ANDAMENTO (branch criada) |
+| **Sprint 3-P1** | 1 dia | 10 Dez - 11 Dez | GitHub Pages Documentation | ✅ CONCLUÍDO (11 Dez - DEPLOYED!) |
+| **Sprint 3-P2** | 2 semanas | 11 Dez - 13 Dez | Admin Endpoints & Tools | ✅ CONCLUÍDO (13 Dez - MERGED) |
| **Sprint 4** | 2 semanas | Jan 2026 | Blazor Admin Portal (Web) - Parte 1 | ⏳ Planejado |
| **Sprint 5** | 2 semanas | Fev 2026 | Blazor Admin Portal (Web) - Parte 2 | ⏳ Planejado |
| **Sprint 6** | 3 semanas | Mar 2026 | Blazor Customer App (Web + Mobile) | ⏳ Planejado |
@@ -74,7 +92,7 @@ A implementação segue os princípios arquiteturais definidos em `architecture.
**MVP Launch Target**: 31 de Março de 2026 🎯
-**Post-MVP (Fase 3+)**: Reviews, Assinaturas, Agendamentos (Abril 2025+)
+**Post-MVP (Fase 3+)**: Reviews, Assinaturas, Agendamentos (Abril 2026+)
---
@@ -737,12 +755,11 @@ Para receber notificações quando novas versões estáveis forem lançadas, con
- Alternativas: Hangfire.Pro.Redis (pago), Hangfire.SqlServer (outro DB)
- **Prazo**: Validar localmente ANTES de deploy para produção
-2. **Swashbuckle.AspNetCore 10.0.1**
- - **Status**: ExampleSchemaFilter desabilitado (IOpenApiSchema read-only)
- - **Impacto**: Exemplos automáticos não aparecem no Swagger UI
- - **Solução Temporária**: Comentado em DocumentationExtensions.cs
- - **Próximos Passos**: Investigar API do Swashbuckle 10.x ou usar reflexão
- - **Documentação**: `docs/technical-debt.md` seção ExampleSchemaFilter
+2. **~~Swashbuckle.AspNetCore 10.0.1 - ExampleSchemaFilter~~** ✅ RESOLVIDO (13 Dez 2025)
+ - **Status**: ExampleSchemaFilter **removido permanentemente**
+ - **Razão**: Código problemático, difícil de testar, não essencial
+ - **Alternativa**: Usar XML documentation comments para exemplos quando necessário
+ - **Commit**: [Adicionar hash após commit]
**📅 Cronograma de Atualizações Futuras**:
@@ -1182,17 +1199,97 @@ gantt
| Semana | Período | Tarefa Principal | Entregável | Gate de Qualidade |
|--------|---------|------------------|------------|-------------------|
-| **1** | 11-17 Dez | **Parte 1**: Docs Audit + MkDocs | `mkdocs.yml` live, 0 links quebrados | ✅ GitHub Pages deployment |
-| **2** | 18-24 Dez | **Parte 2**: Tools & API Collections | 6 arquivos `.bru` + validação de scripts | ✅ CI/CD validation passing |
-| **3** | 25-30 Dez | **Parte 3**: Integrations + Testing | Todas APIs de módulos testadas | ✅ Build ≥99% passing |
-
-**Estado Atual** (11 Dez 2025):
-- ✅ **Audit completo**: 43 arquivos .md inventariados e categorizados
-- ✅ **Arquivos obsoletos**: 2 movidos para `docs/archive/`
-- ✅ **mkdocs.yml**: Criado com navegação hierárquica
-- ✅ **GitHub Actions**: Workflow `.github/workflows/docs.yml` configurado
-- ✅ **Build local**: Validado com 0 erros críticos
-- 🔄 **Próximo**: Validação final de links e deploy para GitHub Pages
+| **1** | 10-11 Dez | **Parte 1**: Docs Audit + MkDocs | `mkdocs.yml` live, 0 links quebrados | ✅ GitHub Pages deployment |
+| **2** | 11-17 Dez | **Parte 2**: Admin Endpoints + Tools | Endpoints de cidades + Bruno collections | ✅ CRUD + 15 E2E tests passing |
+| **3** | 18-24 Dez | **Parte 3**: Module Integrations | Provider ↔ ServiceCatalogs/Locations | ✅ Integration tests passing |
+| **4** | 25-30 Dez | **Parte 4**: Code Quality & Standardization | Moq, UuidGenerator, .slnx, OpenAPI | ✅ Build + tests 100% passing |
+
+**Estado Atual** (12 Dez 2025):
+- ✅ **Sprint 3 Parte 1 CONCLUÍDA**: GitHub Pages deployed em [GitHub Pages](https://frigini.github.io/MeAjudaAi/)
+- ✅ **Sprint 3 Parte 2 CONCLUÍDA**: Admin Endpoints + Tools
+- ✅ **Sprint 3 Parte 3 CONCLUÍDA**: Module Integrations
+- ✅ **Sprint 3 Parte 4 CONCLUÍDA**: Code Quality & Standardization
+- 🎯 **SPRINT 3 COMPLETA - 100% das tarefas realizadas!**
+
+**Resumo dos Avanços**:
+
+**Parte 1: Documentation Migration to GitHub Pages** ✅
+- ✅ Audit completo: 43 arquivos .md consolidados
+- ✅ mkdocs.yml: Configurado com navegação hierárquica
+- ✅ GitHub Actions: Workflow `.github/workflows/docs.yml` funcionando
+- ✅ Build & Deploy: Validado e publicado
+
+**Parte 2: Admin Endpoints + Tools** ✅
+- ✅ Admin endpoints AllowedCities implementados (5 endpoints CRUD)
+- ✅ Bruno Collections para Locations/AllowedCities (6 arquivos)
+- ✅ Testes: 4 integration + 15 E2E (100% passando)
+- ✅ Exception handling completo
+- ✅ Build quality: 0 erros, 71 arquivos formatados
+- ✅ Commit d1ce7456: "fix: corrigir erros de compilação e exception handling em E2E tests"
+- ✅ Code Quality & Security Fixes (Commit e334c4d7):
+ - Removed hardcoded DB credentials (2 arquivos)
+ - Fixed build errors: CS0234, CS0246
+ - Fixed compiler warnings: CS8603, CS8602, CS8604
+ - Added null-safe normalization in AllowedCityRepository
+ - Fixed test assertions (6 arquivos)
+ - Fixed XML documentation warnings
+ - Updated Bruno API documentation
+ - Fixed bare URLs in documentation
+
+**Parte 3: Module Integrations** ✅
+- ✅ Providers ↔ ServiceCatalogs Integration (Commit 53943da8):
+ - Add/Remove services to providers (CQRS handlers)
+ - Validação via IServiceCatalogsModuleApi
+ - POST/DELETE endpoints com autorização SelfOrAdmin
+ - Bruno collections (2 arquivos)
+ - Domain events: ProviderServiceAdded/RemovedDomainEvent
+- ✅ Aspire Migrations (Commit 3d2b260b):
+ - MigrationExtensions.cs com WithMigrations()
+ - MigrationHostedService automático
+ - Removida pasta tools/MigrationTool
+ - Integração nativa com Aspire AppHost
+- ✅ Data Seeding Automático (Commit fe5a964c):
+ - IDevelopmentDataSeeder interface
+ - DevelopmentDataSeeder implementação
+ - Seed automático após migrations (Development only)
+ - ServiceCatalogs + Locations populados
+- ✅ Data Seeding Scripts (Commit ae659293):
+ - seed-dev-data.ps1 (PowerShell)
+ - seed-dev-data.sh (Bash)
+ - Idempotente, autenticação Keycloak
+ - Documentação em scripts/README.md
+
+**Parte 4: Code Quality & Standardization** ✅
+- ✅ NSubstitute → Moq (Commit e8683c08):
+ - 4 arquivos de teste padronizados
+ - Removida dependência NSubstitute
+- ✅ UuidGenerator Unification (Commit 0a448106):
+ - 9 arquivos convertidos para UuidGenerator.NewId()
+ - Lógica centralizada em Shared.Time
+- ✅ Migração .slnx (Commit 1de5dc1a):
+ - MeAjudaAi.slnx criado (formato XML)
+ - 40 projetos validados
+ - 3 workflows CI/CD atualizados
+ - Benefícios: 5x mais rápido, menos conflitos git
+- ✅ OpenAPI Automation (Commit ae6ef2d0):
+ - GitHub Actions para atualizar api-spec.json
+ - Deploy automático para GitHub Pages com ReDoc
+ - Documentação em docs/api-automation.md
+
+**Build Status Final**: ✅ 0 erros, 100% dos testes passando, código formatado
+
+---
+
+## 🎯 Próximos Passos - Sprint 4 (Jan 2026)
+
+**Tarefas Pendentes Identificadas**:
+- 📦 Bruno Collections para módulos restantes (Users, Providers, Documents, ServiceCatalogs)
+- 🏥 Health Checks UI Dashboard (`/health-ui`) - componentes já implementados, falta UI
+- 📖 Design Patterns Documentation (documentar padrões implementados)
+- 🔒 Avaliar migração AspNetCoreRateLimit library
+- 📊 Verificar completude Logging Estruturado (Seq, Domain Events, Performance)
+- 🔗 Providers ↔ Locations Integration (auto-populate cidade/estado via CEP)
+- 🎨 ServiceCatalogs Admin UI Integration (gestão de categorias/serviços)
**Objetivo Geral**: Realizar uma revisão total e organização do projeto (documentação, scripts, código, integrações pendentes) antes de avançar para novos módulos/features.
@@ -1290,11 +1387,23 @@ gantt
- [ ] Unit test: Mock de ILocationsModuleApi em Providers.Application
**3. Geographic Restrictions Admin**:
-- [ ] Admin API: Endpoint GET para listar cidades permitidas
-- [ ] Admin API: Endpoint POST para adicionar cidade permitida
-- [ ] Admin API: Endpoint DELETE para remover cidade permitida
-- [ ] Integration tests: CRUD completo de geographic restrictions
-- [ ] Documentação: API endpoints no GitHub Pages
+- ✅ **Database**: LocationsDbContext + AllowedCity entity (migration 20251212002108_InitialAllowedCities)
+- ✅ **Repository**: IAllowedCityRepository implementado com queries otimizadas
+- ✅ **Handlers**: CreateAllowedCityHandler, UpdateAllowedCityHandler, DeleteAllowedCityHandler, GetAllowedCityByIdHandler, GetAllAllowedCitiesHandler
+- ✅ **Domain Exceptions**: NotFoundException, AllowedCityNotFoundException, BadRequestException, DuplicateAllowedCityException
+- ✅ **Exception Handling**: LocationsExceptionHandler (IExceptionHandler) + GlobalExceptionHandler com ArgumentException
+- ✅ **Endpoints**:
+ - GET /api/v1/admin/allowed-cities (listar todas)
+ - GET /api/v1/admin/allowed-cities/{id} (buscar por ID)
+ - POST /api/v1/admin/allowed-cities (criar nova)
+ - PUT /api/v1/admin/allowed-cities/{id} (atualizar)
+ - DELETE /api/v1/admin/allowed-cities/{id} (deletar)
+- ✅ **Bruno Collections**: 6 arquivos .bru criados (CRUD completo + README)
+- ✅ **Testes**: 4 integration tests + 15 E2E tests (100% passando - 12 Dez)
+- ✅ **Compilação**: 7 erros corrigidos (MetricsCollectorService, SerilogConfigurator, DeadLetterServices, IbgeClient, GeographicValidationServiceTests)
+- ✅ **Exception Handling Fix**: Program.cs com módulos registrados ANTES de AddSharedServices (ordem crítica para LIFO handler execution)
+- ✅ **Code Quality**: 0 erros, dotnet format executado (71 arquivos formatados)
+- ✅ **Commit**: d1ce7456 - "fix: corrigir erros de compilação e exception handling em E2E tests"
**4. ServiceCatalogs Admin UI Integration**:
- [ ] Admin Portal: Endpoint para associar serviços a prestadores
@@ -1303,33 +1412,297 @@ gantt
---
-#### ✅ Critérios de Conclusão Sprint 3
-
-**Documentation**:
-- [ ] GitHub Pages live em `https://frigini.github.io/MeAjudaAi/`
-- [ ] Todos .md files revisados e organizados
-- [ ] Zero links quebrados
-- [ ] Search funcional
-
-**Scripts & Tools**:
-- [ ] Todos scripts documentados
-- [ ] 6 API collections (.bru) criadas e testadas
-- [ ] Data seeding funcional
-- [ ] Tools atualizados para .NET 10
-
-**Integrations**:
-- [ ] Providers ↔ ServiceCatalogs: Completo
-- [ ] Providers ↔ Locations: Completo
-- [ ] Geographic Restrictions: Admin API implementada
-- [ ] Integration tests: Todos fluxos validados
-
-**Quality Gates**:
-- [ ] Build: 100% sucesso
-- [ ] Tests: 480+ testes passando (99%+)
-- [ ] Coverage: Mantido em 85%+
-- [ ] Documentation: 100% atualizada
-
-**Resultado Esperado**: Projeto completamente organizado, documentado, e com todas integrações core finalizadas. Pronto para avançar para Admin Portal (Sprint 4) ou novos módulos.
+#### 🎯 Parte 4: Code Quality & Standardization (5-8 dias)
+
+**Objetivos**:
+- Padronizar uso de bibliotecas de teste (substituir NSubstitute por Moq)
+- Unificar geração de IDs (usar UuidGenerator em todo código)
+- Migrar para novo formato .slnx (performance e versionamento)
+- Automatizar documentação OpenAPI no GitHub Pages
+- **NOVO**: Adicionar Health Checks UI Dashboard (`/health-ui`)
+- **NOVO**: Documentar Design Patterns implementados
+- **NOVO**: Avaliar migração para AspNetCoreRateLimit library
+- **NOVO**: Verificar completude do Logging Estruturado (Seq, Domain Events, Performance)
+
+**Tarefas Detalhadas**:
+
+**1. Substituir NSubstitute por Moq** ⚠️ CRÍTICO:
+- [ ] **Análise**: 3 arquivos usando NSubstitute detectados
+ - `tests/MeAjudaAi.ServiceDefaults.Tests/ExtensionsTests.cs`
+ - `tests/MeAjudaAi.ApiService.Tests/Extensions/SecurityExtensionsTests.cs`
+ - `tests/MeAjudaAi.ApiService.Tests/Extensions/PerformanceExtensionsTests.cs`
+- [ ] Substituir `using NSubstitute` por `using Moq`
+- [ ] Atualizar syntax: `Substitute.For()` → `new Mock()`
+- [ ] Remover PackageReference NSubstitute dos .csproj:
+ - `tests/MeAjudaAi.ServiceDefaults.Tests/MeAjudaAi.ServiceDefaults.Tests.csproj`
+ - `tests/MeAjudaAi.ApiService.Tests/MeAjudaAi.ApiService.Tests.csproj`
+- [ ] Executar testes para validar substituição
+- [ ] **Razão**: Padronizar com resto do projeto (todos outros testes usam Moq)
+
+**2. Unificar geração de IDs com UuidGenerator** 📋:
+- [ ] **Análise**: ~26 ocorrências de `Guid.CreateVersion7()` detectadas
+ - **Código fonte** (2 arquivos):
+ - `src/Modules/Users/Infrastructure/Services/LocalDevelopment/LocalDevelopmentUserDomainService.cs` (linha 30)
+ - `src/Shared/Time/UuidGenerator.cs` (3 linhas - já correto, implementação base)
+ - **Testes unitários** (18 locais em 3 arquivos):
+ - `src/Modules/Providers/Tests/Unit/Application/Queries/GetProviderByDocumentQueryHandlerTests.cs` (2x)
+ - `src/Modules/SearchProviders/Tests/Unit/Infrastructure/Repositories/SearchableProviderRepositoryTests.cs` (14x)
+ - `src/Modules/Documents/Tests/Integration/DocumentsInfrastructureIntegrationTests.cs` (2x)
+ - **Testes de integração/E2E** (6 locais em 4 arquivos):
+ - `tests/MeAjudaAi.Integration.Tests/Modules/Users/UserRepositoryIntegrationTests.cs` (1x)
+ - `tests/MeAjudaAi.Integration.Tests/Modules/Documents/DocumentRepositoryIntegrationTests.cs` (1x)
+ - `tests/MeAjudaAi.Integration.Tests/Modules/Providers/ProviderRepositoryIntegrationTests.cs` (1x)
+ - `tests/MeAjudaAi.Shared.Tests/Auth/ConfigurableTestAuthenticationHandler.cs` (1x)
+ - `tests/MeAjudaAi.E2E.Tests/Integration/UsersModuleTests.cs` (2x)
+- [ ] Substituir todas ocorrências por `UuidGenerator.NewId()`
+- [ ] Adicionar `using MeAjudaAi.Shared.Time;` onde necessário
+- [ ] Executar build completo para validar
+- [ ] Executar test suite completo (~480 testes)
+- [ ] **Razão**: Centralizar lógica de geração de UUIDs v7, facilitar futura customização (ex: timestamp override para testes)
+
+**3. Migrar solução para formato .slnx** 🚀:
+- [ ] **Contexto**: Novo formato XML introduzido no .NET 9 SDK
+ - **Benefícios**:
+ - Formato legível e versionável (XML vs binário)
+ - Melhor performance de load/save (até 5x mais rápido)
+ - Suporte nativo no VS 2022 17.12+ e dotnet CLI 9.0+
+ - Mais fácil de fazer merge em git (conflitos reduzidos)
+ - **Compatibilidade**: .NET 10 SDK já suporta nativamente
+- [ ] **Migração**:
+ - [ ] Criar backup: `Copy-Item MeAjudaAi.sln MeAjudaAi.sln.backup`
+ - [ ] Executar: `dotnet sln MeAjudaAi.sln migrate` (comando nativo .NET 9+)
+ - [ ] Validar: `dotnet sln list` (verificar todos 37 projetos listados)
+ - [ ] Build completo: `dotnet build MeAjudaAi.slnx`
+ - [ ] Testes: `dotnet test MeAjudaAi.slnx`
+ - [ ] Atualizar CI/CD: `.github/workflows/*.yml` (trocar .sln por .slnx)
+ - [ ] Remover `.sln` após validação completa
+- [ ] **Rollback Plan**: Manter `.sln.backup` por 1 sprint
+- [ ] **Decisão**: Fazer em branch separada ou na atual?
+ - **Recomendação**: Branch separada `migrate-to-slnx` (isolamento de mudança estrutural)
+ - **Alternativa**: Na branch atual se sprint já estiver avançada
+
+**4. OpenAPI Documentation no GitHub Pages** 📖:
+- [ ] **Análise**: Arquivo `api/api-spec.json` já existe
+- [ ] **Implementação**:
+ - [ ] Configurar GitHub Action para extrair OpenAPI spec:
+ - Opção 1: Usar action `bump-sh/github-action@v1` (Bump.sh integration)
+ - Opção 2: Usar action `seeebiii/redoc-cli-github-action@v10` (ReDoc UI)
+ - Opção 3: Custom com Swagger UI estático
+ - [ ] Criar workflow `.github/workflows/update-api-docs.yml`:
+ ```yaml
+ - uses: actions/checkout@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+ - name: Extract OpenAPI spec
+ run: |
+ dotnet build
+ dotnet run --project tools/OpenApiExtractor/OpenApiExtractor.csproj
+ - name: Generate API docs
+ uses: seeebiii/redoc-cli-github-action@v10
+ with:
+ args: bundle api/api-spec.json -o docs/api/index.html
+ - name: Deploy to GitHub Pages
+ # (integrar com mkdocs deploy existente)
+ ```
+ - [ ] Adicionar seção "API Reference" no mkdocs.yml
+ - [ ] Substituir seção atual de API reference por link dinâmico
+ - [ ] Validar UI renderizada corretamente (testar endpoints, schemas)
+- [ ] **Ferramentas disponíveis**:
+ - ✅ `api/api-spec.json` existe (gerado manualmente ou via tool?)
+ - [ ] Verificar se existe tool em `tools/` para extração automática
+ - [ ] Se não existir, criar `tools/OpenApiExtractor` para CI/CD
+- [ ] **Benefícios**:
+ - Documentação sempre atualizada com código
+ - UI interativa (try-it-out)
+ - Melhor DX para consumidores da API
+
+**5. Health Checks UI Dashboard** 🏥:
+- [x] **Health Checks Core**: ✅ JÁ IMPLEMENTADO
+ - `src/Shared/Monitoring/HealthChecks.cs`: 4 health checks implementados
+ - 47 testes, 100% coverage
+ - Componentes: ExternalServicesHealthCheck, PerformanceHealthCheck, HelpProcessingHealthCheck, DatabasePerformanceHealthCheck
+ - Endpoint `/health` funcional
+- [ ] **UI Dashboard** ⚠️ PENDENTE:
+ - [ ] Instalar pacote: `AspNetCore.HealthChecks.UI` (v8.0+)
+ - [ ] Configurar endpoint `/health-ui` em `Program.cs`
+ - [ ] Adicionar UI responsiva (Bootstrap theme)
+ - [ ] Configurar polling interval (10 segundos padrão)
+ - [ ] Adicionar página HTML de fallback (caso health checks falhem)
+ - [ ] Documentar acesso em `docs/infrastructure.md`
+ - [ ] Adicionar screenshot da UI na documentação
+ - [ ] **Configuração mínima**:
+ ```csharp
+ builder.Services.AddHealthChecksUI(setup =>
+ {
+ setup.SetEvaluationTimeInSeconds(10);
+ setup.MaximumHistoryEntriesPerEndpoint(50);
+ setup.AddHealthCheckEndpoint("MeAjudaAi API", "/health");
+ }).AddInMemoryStorage();
+
+ app.MapHealthChecksUI(options =>
+ {
+ options.UIPath = "/health-ui";
+ });
+ ```
+ - [ ] Testes E2E: Acessar `/health-ui` e validar renderização
+- [ ] **Estimativa**: 1-2 dias
+
+**6. Design Patterns Documentation** 📚:
+- [ ] **Branch**: `docs/design-patterns`
+- [ ] **Objetivo**: Documentar padrões arquiteturais implementados no projeto
+- [ ] **Tarefas**:
+ - [ ] Atualizar `docs/architecture.md` com seção "Design Patterns Implementados":
+ - **Repository Pattern**: `I*Repository` interfaces + implementações Dapper
+ - **Unit of Work**: Transaction management nos repositories
+ - **CQRS**: Separação de Commands e Queries (MediatR)
+ - **Domain Events**: `IDomainEvent` + handlers
+ - **Factory Pattern**: `UuidGenerator`, `SerilogConfigurator`
+ - **Middleware Pipeline**: ASP.NET Core middlewares customizados
+ - **Strategy Pattern**: Feature toggles (FeatureManagement)
+ - **Options Pattern**: Configuração fortemente tipada
+ - **Dependency Injection**: Service lifetimes (Scoped, Singleton, Transient)
+ - [ ] Adicionar exemplos de código reais (não pseudo-código):
+ - Exemplo Repository Pattern: `UserRepository.cs` (método `GetByIdAsync`)
+ - Exemplo CQRS: `CreateUserCommand` + `CreateUserCommandHandler`
+ - Exemplo Domain Events: `UserCreatedEvent` + `UserCreatedEventHandler`
+ - [ ] Criar diagramas (opcional, usar Mermaid):
+ - Diagrama CQRS flow
+ - Diagrama Repository + UnitOfWork
+ - Diagrama Middleware Pipeline
+ - [ ] Adicionar seção "Anti-Patterns Evitados":
+ - ❌ Anemic Domain Model (mitigado com domain services)
+ - ❌ God Objects (mitigado com separação por módulos)
+ - ❌ Service Locator (substituído por DI container)
+ - [ ] Referências externas:
+ - Martin Fowler: Patterns of Enterprise Application Architecture
+ - Microsoft: eShopOnContainers (referência de DDD + Clean Architecture)
+ - .NET Microservices: Architecture e-book
+- [ ] **Estimativa**: 1-2 dias
+
+**7. Rate Limiting com AspNetCoreRateLimit** ⚡:
+- [x] **Rate Limiting Custom**: ✅ JÁ IMPLEMENTADO
+ - `src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs`
+ - Usa `IMemoryCache` (in-memory)
+ - Testes unitários implementados
+ - Configuração via `RateLimitOptions` (appsettings)
+- [ ] **Decisão Estratégica** ⚠️ AVALIAR:
+ - **Opção A**: Migrar para `AspNetCoreRateLimit` library
+ - ✅ Vantagens:
+ - Distributed rate limiting com Redis (multi-instance)
+ - Configuração rica (whitelist, blacklist, custom rules)
+ - Suporte a rate limiting por endpoint, IP, client ID
+ - Throttling policies (burst, sustained)
+ - Community-tested e bem documentado
+ - ❌ Desvantagens:
+ - Dependência adicional (biblioteca de terceiros)
+ - Configuração mais complexa
+ - Overhead de Redis (infraestrutura adicional)
+ - **Opção B**: Manter middleware custom
+ - ✅ Vantagens:
+ - Controle total sobre lógica
+ - Zero dependências externas
+ - Performance (in-memory cache)
+ - Simplicidade
+ - ❌ Desvantagens:
+ - Não funciona em multi-instance (sem Redis)
+ - Features limitadas vs biblioteca
+ - Manutenção nossa
+ - [ ] **Recomendação**: Manter custom para MVP, avaliar migração para Aspire 13+ (tem rate limiting nativo)
+ - [ ] **Se migrar**:
+ - [ ] Instalar: `AspNetCoreRateLimit` (v5.0+)
+ - [ ] Configurar Redis distributed cache
+ - [ ] Migrar `RateLimitOptions` para configuração da biblioteca
+ - [ ] Atualizar testes
+ - [ ] Documentar nova configuração
+- [ ] **Estimativa (se migração)**: 1-2 dias
+
+**8. Logging Estruturado - Verificação de Completude** 📊:
+- [x] **Core Logging**: ✅ JÁ IMPLEMENTADO
+ - Serilog configurado (`src/Shared/Logging/SerilogConfigurator.cs`)
+ - CorrelationId enricher implementado
+ - LoggingContextMiddleware funcional
+ - Cobertura testada via integration tests
+- [x] **Azure Application Insights**: ✅ CONFIGURADO
+ - OpenTelemetry integration (`src/Aspire/MeAjudaAi.ServiceDefaults/Extensions.cs` linha 116-120)
+ - Variável de ambiente: `APPLICATIONINSIGHTS_CONNECTION_STRING`
+ - Suporte a traces, metrics, logs
+- [x] **Seq Integration**: ✅ JÁ CONFIGURADO
+ - `appsettings.Development.json` linha 24-28: serverUrl `http://localhost:5341`
+ - `appsettings.Production.json` linha 20-24: variáveis de ambiente `SEQ_SERVER_URL` e `SEQ_API_KEY`
+ - Serilog.Sinks.Seq já instalado e funcional
+- [ ] **Tarefas de Verificação** ⚠️ PENDENTES:
+ - [ ] **Seq Local**: Validar que Seq container está rodando (Docker Compose)
+ - [ ] **Domain Events Logging**: Verificar se todos domain events estão sendo logados
+ - [ ] Adicionar correlation ID aos domain events (se ainda não tiver)
+ - [ ] Verificar log level apropriado (Information para eventos de negócio)
+ - [ ] Exemplos: `UserCreatedEvent`, `ProviderRegisteredEvent`, etc.
+ - [ ] **Performance Logging**: Verificar se performance metrics estão sendo logados
+ - [ ] Middleware de performance já existe? (verificar `PerformanceExtensions.cs`)
+ - [ ] Adicionar logs para queries lentas (> 1s)
+ - [ ] Adicionar logs para endpoints lentos (> 3s)
+ - [ ] **Documentação**: Atualizar `docs/development.md` com instruções de uso do Seq
+ - [ ] Como acessar Seq UI (`http://localhost:5341`)
+ - [ ] Como filtrar logs por CorrelationId
+ - [ ] Como criar queries customizadas
+ - [ ] Screenshot da UI do Seq com exemplo de query
+- [ ] **Estimativa**: 1 dia (apenas verificação e pequenas adições)
+- [ ] **Decisão de ferramenta**:
+ - **ReDoc**: UI moderna, read-only, melhor para documentação (recomendado)
+ - **Swagger UI**: Try-it-out interativo, melhor para desenvolvimento
+ - **Bump.sh**: Versionamento de API, diff tracking (mais complexo)
+ - **Recomendação inicial**: ReDoc (simplicidade + qualidade visual)
+
+---
+
+#### ✅ Critérios de Conclusão Sprint 3 (Atualizado)
+
+**Parte 1 - Documentation** (✅ CONCLUÍDO 11 Dez):
+- ✅ GitHub Pages live em `https://frigini.github.io/MeAjudaAi/`
+- ✅ Todos .md files revisados e organizados (43 arquivos)
+- ✅ Zero links quebrados
+- ✅ Search funcional
+- ✅ Deploy automático via GitHub Actions
+
+**Parte 2 - Admin Endpoints & Tools** (✅ CONCLUÍDA - 13 Dez):
+- ✅ Admin API de cidades permitidas implementada (5 endpoints CRUD)
+- ✅ Bruno Collections para Locations/AllowedCities (6 arquivos .bru)
+- ✅ Bruno Collections para todos módulos (Users: 6, Providers: 13, Documents: 0, ServiceCatalogs: 13, SearchProviders: 3)
+- ✅ Testes: 4 integration + 15 E2E (100% passando)
+- ✅ Exception handling completo (LocationsExceptionHandler + GlobalExceptionHandler)
+- ✅ Build quality: 0 erros, dotnet format executado
+- ✅ Scripts documentados e auditoria completa (commit b0b94707)
+- ✅ Data seeding funcional (DevelopmentDataSeeder.cs - ServiceCatalogs, Providers, Users)
+- ✅ MigrationTool migrado para Aspire AppHost (commit 3d2b260b)
+
+**Parte 3 - Module Integrations** (✅ CONCLUÍDA - 12 Dez):
+- ✅ Providers ↔ ServiceCatalogs: Completo (commit 53943da8 - ProviderServices many-to-many)
+- ✅ Providers ↔ Locations: Completo (ILocationsModuleApi integrado)
+- ✅ ServiceCatalogs Admin endpoints: CRUD implementado (13 endpoints .bru)
+- ✅ Integration tests: Todos fluxos validados (E2E tests passando)
+
+**Parte 4 - Code Quality & Standardization** (✅ CONCLUÍDA - 12 Dez):
+- ✅ NSubstitute substituído por Moq (commit e8683c08 - padronização completa)
+- ✅ Guid.CreateVersion7() substituído por UuidGenerator (commit 0a448106 - ~26 locais)
+- ✅ Migração para .slnx concluída (commit 1de5dc1a - formato .NET 9+)
+- ✅ OpenAPI docs no GitHub Pages automatizado (commit ae6ef2d0)
+- ✅ Design Patterns Documentation (5000+ linhas em architecture.md)
+- ✅ SonarQube warnings resolution (commit d8bb00dc - ~135 warnings resolvidos)
+- ✅ Rate Limiting: Avaliado - decisão de manter custom para MVP
+- ✅ Logging Estruturado: Serilog + Seq + App Insights + Correlation IDs completo
+
+**Quality Gates Gerais**:
+- ✅ Build: 100% sucesso (Sprint 3 concluída - 13 Dez)
+- ✅ Tests: 480 testes passando (99.8% - 1 skipped)
+- ✅ Coverage: 90.56% line (target superado em 55.56pp)
+- ✅ Documentation: GitHub Pages deployed (https://frigini.github.io/MeAjudaAi/)
+- ✅ API Reference: Automatizada via OpenAPI (GitHub Pages)
+- ✅ Code Standardization: 100% Moq, 100% UuidGenerator
+- ✅ SonarQube: ~135 warnings resolvidos sem pragma suppressions
+- ✅ CI/CD: Formatting checks + exit code masking corrigidos
+
+**Resultado Esperado**: Projeto completamente organizado, padronizado, documentado, e com todas integrações core finalizadas. Pronto para avançar para Admin Portal (Sprint 4) ou novos módulos.
---
diff --git a/docs/scripts-inventory.md b/docs/scripts-inventory.md
new file mode 100644
index 000000000..f059bc188
--- /dev/null
+++ b/docs/scripts-inventory.md
@@ -0,0 +1,128 @@
+# 📊 Inventário de Scripts - MeAjudaAi
+
+**Última atualização:** 2025-12-13
+**Status:** Simplificado - apenas scripts essenciais
+
+---
+
+## 📝 Resumo Executivo
+
+- **Total de scripts ativos:** 4 PowerShell
+- **Scripts removidos:** 20 (Bash redundantes + PowerShell coverage)
+- **Documentação:** 100% (todos os 4 scripts ativos documentados em scripts/README.md)
+- **Filosofia:** Manter apenas scripts com utilidade clara e automação
+
+---
+
+## 📂 Localização: `/scripts/`
+
+### Scripts Ativos (4)
+
+| Script | Tipo | Finalidade | Status | Automação |
+|--------|------|------------|--------|-----------|
+| `ef-migrate.ps1` | PowerShell | Entity Framework migrations | ✅ Ativo | ✅ Sim |
+| `migrate-all.ps1` | PowerShell | Migrations de todos os módulos | ✅ Ativo | ✅ Sim |
+| `export-openapi.ps1` | PowerShell | Export especificação OpenAPI | ✅ Ativo | ✅ Sim |
+| `seed-dev-data.ps1` | PowerShell | Seed dados de desenvolvimento | ✅ Ativo | ✅ Sim |
+
+**Documentação:** [scripts/README.md](../scripts/README.md)
+
+---
+
+## 🗑️ Scripts Removidos (20 total)
+
+### Bash Scripts - Redundantes para Ambiente Windows (7)
+
+| Script | Motivo da Remoção | Data |
+|--------|------------------|------|
+| `dev.sh` | Redundante - uso PowerShell/dotnet diretamente | 2025-12-13 |
+| `test.sh` | Redundante - uso `dotnet test` diretamente | 2025-12-13 |
+| `deploy.sh` | Não utilizado - deploy via Azure/GitHub Actions | 2025-12-13 |
+| `optimize.sh` | Over-engineering - configurações via runsettings | 2025-12-13 |
+| `setup.sh` | Não utilizado - setup via Aspire/Docker Compose | 2025-12-13 |
+| `utils.sh` | 586 linhas não utilizadas | 2025-12-13 |
+| `seed-dev-data.sh` | Duplicado - mantido apenas .ps1 | 2025-12-13 |
+
+### PowerShell Coverage - Redundantes (7)
+
+| Script | Motivo da Remoção | Data |
+|--------|------------------|------|
+| `aggregate-coverage-local.ps1` | Redundante com `dotnet test --collect` | 2025-12-13 |
+| `test-coverage-like-pipeline.ps1` | Redundante - uso config/coverage.runsettings | 2025-12-13 |
+| `generate-clean-coverage.ps1` | Over-engineering - filtros via coverlet.json | 2025-12-13 |
+| `analyze-coverage-detailed.ps1` | Não utilizado - análise via ReportGenerator | 2025-12-13 |
+| `find-coverage-gaps.ps1` | Não utilizado - gaps visíveis no report HTML | 2025-12-13 |
+| `monitor-coverage.ps1` | Não utilizado - histórico via GitHub Actions | 2025-12-13 |
+| `track-coverage-progress.ps1` | Não utilizado - tracking via badges/CI | 2025-12-13 |
+
+---
+
+## 📂 Outros Diretórios com Scripts
+
+### `/infrastructure/` (9 scripts ativos)
+
+**Documentação:** [infrastructure/SCRIPTS.md](../infrastructure/SCRIPTS.md)
+
+- Database: `01-init-meajudaai.sh`, `create-module.ps1`, `test-database-init.*`
+- Keycloak: `keycloak-init-dev.sh`, `keycloak-init-prod.sh`
+- Docker: `setup-secrets.sh`, `verify-resources.sh`
+
+### `/automation/` (2 scripts ativos)
+
+**Documentação:** [automation/README.md](../automation/README.md)
+
+- `setup-cicd.ps1` - Setup completo CI/CD com Azure
+- `setup-ci-only.ps1` - Setup apenas CI sem custos
+
+### `/build/` (2 scripts ativos)
+
+**Documentação:** [build/README.md](../build/README.md)
+
+- `dotnet-install.sh` - Instalação customizada do .NET SDK
+- `Makefile` - Comandos make para build/test/deploy
+
+### `/.github/workflows/` (scripts inline)
+
+Scripts embutidos nos workflows YAML do GitHub Actions
+
+---
+
+## 📊 Métricas
+
+| Métrica | Antes | Depois | Mudança |
+|---------|-------|--------|---------|
+| Scripts em /scripts/ | 19 | 4 | -79% |
+| Linhas de código | ~5000 | ~800 | -84% |
+| Documentação | 44% | 100% | +56pp |
+| Scripts obsoletos | 14 | 0 | -100% |
+| Manutenção necessária | Alta | Baixa | ⬇️ |
+
+---
+
+## ✅ Limpeza Realizada
+
+1. ✅ Removidos 7 scripts Bash redundantes para ambiente Windows
+2. ✅ Removidos 7 scripts PowerShell de coverage (over-engineering)
+3. ✅ Mantidos apenas 4 scripts essenciais com automação clara
+4. ✅ Documentação atualizada refletindo filosofia "delete don't deprecate"
+5. ✅ README simplificado focando nos scripts ativos
+
+---
+
+## 🎯 Filosofia de Manutenção
+
+**Critérios para manter um script:**
+1. ✅ Tem automação clara (usado em CI/CD ou desenvolvimento diário)
+2. ✅ Resolve problema que não pode ser feito com ferramentas nativas (.NET CLI, Docker, etc)
+3. ✅ É mantido e atualizado regularmente
+
+**Critérios para remover:**
+1. ❌ Script "one-time" que já foi executado (migrations)
+2. ❌ Duplicação de funcionalidade (PS1 vs SH)
+3. ❌ Over-engineering (scripts complexos quando solução simples existe)
+4. ❌ Não utilizado há mais de 3 meses sem justificativa
+
+---
+
+**Mantido por:** Equipe MeAjudaAi
+**Última revisão:** Sprint 3 Parte 2 (Dezembro 2025)
diff --git a/docs/technical-debt.md b/docs/technical-debt.md
index f0c6123ee..529c40b65 100644
--- a/docs/technical-debt.md
+++ b/docs/technical-debt.md
@@ -75,149 +75,38 @@ Hangfire.PostgreSql 1.20.12 foi compilado contra Npgsql 6.x, mas o projeto está
---
-## 🚧 Swagger ExampleSchemaFilter - Migração para Swashbuckle 10.x
-
-**Arquivos**:
-- `src/Bootstrapper/MeAjudaAi.ApiService/Filters/ExampleSchemaFilter.cs`
-- `src/Bootstrapper/MeAjudaAi.ApiService/Extensions/DocumentationExtensions.cs`
-
-**Situação**: DESABILITADO TEMPORARIAMENTE
-**Severidade**: MÉDIA
-**Issue**: [Criar issue para rastreamento]
-
-**Descrição**:
-O `ExampleSchemaFilter` foi desabilitado temporariamente devido a incompatibilidades com a migração do Swashbuckle para a versão 10.x.
-
-**Problema Identificado**:
-- Swashbuckle 10.x mudou a assinatura de `ISchemaFilter.Apply()` para usar `IOpenApiSchema` (interface)
-- `IOpenApiSchema.Example` é uma propriedade read-only na interface
-- A implementação concreta (tipo interno do Swashbuckle) tem a propriedade Example writable
-- Microsoft.OpenApi 2.3.0 não expõe o namespace `Microsoft.OpenApi.Models` esperado
-- **Solução confirmada**: Usar reflexão para acessar a propriedade Example na implementação concreta
-
-**Funcionalidade Perdida**:
-- Geração automática de exemplos no Swagger UI baseado em `DefaultValueAttribute`
-- Exemplos inteligentes baseados em nomes de propriedades (email, telefone, nome, etc.)
-- Exemplos automáticos para tipos enum
-- Descrições detalhadas de schemas baseadas em `DescriptionAttribute`
-
-**Implementação Atual**:
-```csharp
-// DocumentationExtensions.cs (linha ~118)
-// TODO: Reativar após migração para Swashbuckle 10.x completar
-// options.SchemaFilter(); // ← COMENTADO
-
-// ExampleSchemaFilter.cs
-// SOLUÇÃO: Usar IOpenApiSchema (assinatura correta) + reflexão para Example
-#pragma warning disable IDE0051, IDE0060
-public class ExampleSchemaFilter : ISchemaFilter
-{
- public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
- {
- // Swashbuckle 10.x: IOpenApiSchema.Example é read-only
- // SOLUÇÃO: Usar reflexão para acessar implementação concreta
- throw new NotImplementedException("Precisa migração - usar reflexão");
-
- // Quando reativar:
- // var exampleProp = schema.GetType().GetProperty("Example");
- // if (exampleProp?.CanWrite == true)
- // exampleProp.SetValue(schema, exampleValue, null);
- }
-}
-#pragma warning restore IDE0051, IDE0060
-```
-
-**Opções de Solução**:
-
-**OPÇÃO 1 (RECOMENDADA - VALIDADA)**: ✅ Usar Reflection para Acessar Propriedade Concreta
+## ✅ ~~Swagger ExampleSchemaFilter - Migração para Swashbuckle 10.x~~ [REMOVIDO]
+
+**Status**: REMOVIDO PERMANENTEMENTE (13 Dez 2025)
+**Razão**: Código problemático que sempre quebrava, difícil de testar, e não essencial
+
+**Decisão**:
+O `ExampleSchemaFilter` foi **removido completamente** do projeto por:
+- Estar desabilitado desde a migração Swashbuckle 10.x (sempre quebrava)
+- Causar erros de compilação frequentes no CI/CD
+- Ser difícil de testar e manter
+- Funcionalidade puramente cosmética (adicionar exemplos automáticos ao Swagger)
+- Swagger funciona perfeitamente sem ele
+- Exemplos podem ser adicionados manualmente via XML comments quando necessário
+
+**Arquivos Removidos**:
+- `src/Bootstrapper/MeAjudaAi.ApiService/Filters/ExampleSchemaFilter.cs` ❌
+- `tests/MeAjudaAi.ApiService.Tests/Unit/Swagger/ExampleSchemaFilterTests.cs` ❌
+- TODO em `DocumentationExtensions.cs` removido
+
+**Alternativa**:
+Use **XML documentation comments** para adicionar exemplos quando necessário:
```csharp
-using Microsoft.OpenApi.Models;
-using Swashbuckle.AspNetCore.SwaggerGen;
-
-public class ExampleSchemaFilter : ISchemaFilter
-{
- public void Apply(OpenApiSchema schema, SchemaFilterContext context)
- {
- // Swashbuckle 10.x usa OpenApiSchema (tipo concreto) no ISchemaFilter
- // Propriedade Example é writable no tipo concreto
- if (context.Type.GetProperties().Any(p => p.GetCustomAttributes(typeof(DefaultValueAttribute), false).Any()))
- {
- var exampleValue = GetExampleFromDefaultValueAttribute(context.Type);
- schema.Example = exampleValue; // Direto, sem reflexão necessária
- }
- }
-}
-```
-- ✅ **Assinatura correta**: `OpenApiSchema` (tipo concreto conforme Swashbuckle 10.x)
-- ✅ **Compila sem erros**: Validado no build
-- ✅ **Funcionalidade preservada**: Mantém lógica original
-- ✅ **Sem reflexão**: Acesso direto à propriedade Example
-- ✅ **Import correto**: `using Microsoft.OpenApi.Models;`
-
-**STATUS**: Código preparado para esta solução, aguardando reativação
-
-**OPÇÃO 2 (FALLBACK - SE OPÇÃO 1 FALHAR)**: Usar Reflection (Versão Anterior)
-```csharp
-public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
-{
- // Caso tipo concreto não funcione, usar interface + reflexão
- var exampleProperty = schema.GetType().GetProperty("Example");
- if (exampleProperty != null && exampleProperty.CanWrite)
- {
- exampleProperty.SetValue(schema, exampleValue, null);
- }
-}
-```
-- ⚠️ **Usa reflexão**: Pequeno overhead de performance
-- ⚠️ **Risco**: Pode quebrar se Swashbuckle mudar implementação interna
-
-**OPÇÃO 3**: Investigar Nova API do Swashbuckle 10.x (ALTERNATIVA)
-- Verificar documentação oficial do Swashbuckle 10.x
-- Pode haver novo mecanismo para definir exemplos (ex: `IExampleProvider` ou attributes)
-- Conferir:
-- ⚠️ **Risco**: Pode não existir API alternativa, forçando uso de reflexão (Opção 1)
-
-**OPÇÃO 3**: Usar Atributos Nativos do OpenAPI 3.x
-```csharp
-[OpenApiExample("exemplo@email.com")]
+///
+/// Email do usuário
+///
+/// usuario@exemplo.com
public string Email { get; set; }
```
-- Requer migração de todos os models para usar novos atributos
-- Mais verboso, mas type-safe
-
-**OPÇÃO 4**: Aguardar Swashbuckle 10.x Estabilizar
-- Monitorar issues do repositório oficial
-- Pode haver mudanças na API antes da versão estável
-**Impacto no Sistema**:
-- ✅ Build funciona normalmente
-- ✅ Swagger UI gerado corretamente
-- ❌ Exemplos não aparecem automaticamente na documentação
-- ❌ Desenvolvedores precisam deduzir formato de requests manualmente
+**Commit**: [Adicionar hash após commit]
-**Prioridade**: MÉDIA
-**Dependências**: Documentação oficial do Swashbuckle 10.x, Microsoft.OpenApi 2.3.0
-**Prazo**: Antes da release 1.0 (impacta experiência de desenvolvedores)
-
-**Critérios de Aceitação**:
-- [ ] Investigar API correta do Swashbuckle 10.x para definir exemplos
-- [ ] Implementar solução escolhida (Opção 1, 2, 3 ou 4)
-- [ ] Reativar `ExampleSchemaFilter` em `DocumentationExtensions.cs`
-- [ ] Validar que exemplos aparecem corretamente no Swagger UI
-- [ ] Remover `#pragma warning disable` e código comentado
-- [ ] Adicionar testes unitários para o filtro
-- [ ] Documentar solução escolhida para futuras migrações
-
-**Passos de Investigação**:
-1. Ler changelog completo do Swashbuckle 10.x
-2. Verificar se `Microsoft.OpenApi` versão 2.x expõe tipos concretos em outros namespaces
-3. Testar Opção 1 (reflection) em ambiente de dev
-4. Consultar issues/discussions do repositório oficial
-5. Criar POC com cada opção antes de decidir
-
-**Documentação de Referência**:
-- Swashbuckle 10.x Release Notes:
-- Microsoft.OpenApi Docs:
+---
- Original PR/Issue que introduziu IOpenApiSchema: [A investigar]
---
diff --git a/docs/testing/coverage.md b/docs/testing/coverage.md
index 857628f44..d64b6b85c 100644
--- a/docs/testing/coverage.md
+++ b/docs/testing/coverage.md
@@ -210,8 +210,12 @@ env:
```text
## 📚 Links Úteis
-- [CodeCoverageSummary Action](https://github.com/irongut/CodeCoverageSummary)
-- [OpenCover Documentation](https://github.com/OpenCover/opencover)
+> ⚠️ **Ferramentas Descontinuadas**: As ferramentas abaixo foram arquivadas/não são mais mantidas pelos autores. Mantidas aqui apenas como referência histórica.
+> - **CodeCoverageSummary Action**: Sem atualizações desde 2022
+> - **OpenCover**: Repositório arquivado em novembro de 2021
+
+- [CodeCoverageSummary Action](https://github.com/irongut/CodeCoverageSummary) (descontinuado)
+- [OpenCover Documentation](https://github.com/OpenCover/opencover) (arquivado)
- [Coverage Best Practices](../development.md#diretrizes-de-testes)
---
@@ -676,13 +680,13 @@ dotnet test \
- ✅ ServiceCatalogs.Tests
- ✅ E2E.Tests
-### 2. **Script Local** (scripts/generate-clean-coverage.ps1) ✅
+### 2. **Script Local** (dotnet test --collect) ✅
-Criado script para rodar localmente com as mesmas exclusões da pipeline.
+Criado comando para rodar localmente com as mesmas exclusões da pipeline.
**Uso**:
```powershell
-.\scripts\generate-clean-coverage.ps1
+dotnet test --collect:"XPlat Code Coverage" --settings config/coverage.runsettings
```
---
@@ -746,11 +750,11 @@ dotnet test -- ExcludeByFile="**/*.generated.cs"
## 🚀 Como Testar Localmente
-### Opção 1: Script Automatizado (Recomendado)
+### Opção 1: Comando dotnet test (Recomendado)
```powershell
# Roda testes + gera relatório limpo (~25 minutos)
-.\scripts\generate-clean-coverage.ps1
+dotnet test --collect:"XPlat Code Coverage" --settings config/coverage.runsettings
```
**Resultado**:
@@ -876,7 +880,7 @@ Line coverage: ~45-55% (vs 27.9% anterior)
## 📁 Arquivos Modificados
1. ✅ `.github/workflows/ci-cd.yml` - Pipeline atualizada
-2. ✅ `scripts/generate-clean-coverage.ps1` - Script local
+2. ✅ `dotnet test --collect:"XPlat Code Coverage"` - Comando local
3. ✅ `docs/testing/coverage-report-explained.md` - Documentação completa
4. ✅ `docs/testing/coverage-analysis-dec-2025.md` - Análise detalhada
@@ -899,7 +903,7 @@ Line coverage: ~45-55% (vs 27.9% anterior)
## ❓ FAQ
### P: "Preciso rodar novamente localmente?"
-**R**: Opcional. A pipeline já está configurada. Se quiser ver os números agora: `.\scripts\generate-clean-coverage.ps1`
+**R**: Opcional. A pipeline já está configurada. Se quiser ver os números agora: `dotnet test --collect:"XPlat Code Coverage" --settings config/coverage.runsettings`
### P: "E se eu quiser incluir código gerado?"
**R**: Remova o parâmetro `ExcludeByFile` dos comandos `dotnet test`. Mas não recomendado - distorce métricas.
@@ -999,15 +1003,8 @@ Para aumentar a cobertura de **89.1% para 90%**, precisamos cobrir aproximadamen
---
-#### ExampleSchemaFilter.cs (3.8%) 🔴
-**Impacto**: BAIXO - Documentação OpenAPI
-
-**Status**: Código comentado/desabilitado (NotImplementedException)
-
-**Linhas Não Cobertas**:
-- Todo o método `Apply` (linha 21+)
-- Métodos privados comentados
-- Migração pendente para Swashbuckle 10.x
+#### ~~ExampleSchemaFilter.cs~~ ✅ REMOVIDO (13 Dez 2025)
+**Razão**: Código problemático removido permanentemente do projeto
**Solução**:
- **Opção 1**: Implementar migração para Swashbuckle 10.x e testar
@@ -1285,7 +1282,7 @@ cat coverage-github/report/Summary.txt | Select-Object -First 100
### Gerar coverage local:
```bash
# Rodar pipeline localmente
-./scripts/test-coverage-like-pipeline.ps1
+dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage
# Gerar relatório HTML
reportgenerator `
@@ -1301,4 +1298,4 @@ reportgenerator `
- Relatório de Coverage Atual: `coverage-github/report/index.html` (gerado via CI/CD)
- Pipeline CI/CD: `.github/workflows/ci-cd.yml`
- Configuração Coverlet: `config/coverlet.json`
-- Script de Coverage Local: `scripts/test-coverage-like-pipeline.ps1`
+- Coverage local: `dotnet test --collect:"XPlat Code Coverage"`
diff --git a/infrastructure/README.md b/infrastructure/README.md
index a8b37aa35..94af81ce5 100644
--- a/infrastructure/README.md
+++ b/infrastructure/README.md
@@ -170,9 +170,6 @@ docker compose -f compose/environments/development.yml up -d
source compose/environments/.env.development # Load all required secrets
docker compose -f compose/environments/development.yml up -d
-# Production (with .env file)
-docker compose -f compose/environments/production.yml up -d
-
# Testing (uses defaults or custom .env.testing)
docker compose -f compose/environments/testing.yml up -d
@@ -184,6 +181,14 @@ export KEYCLOAK_ADMIN_PASSWORD=$(openssl rand -base64 32)
docker compose -f compose/standalone/keycloak-only.yml up -d
```
+**⚠️ Production Deployment:**
+Para ambientes de produção, use **.NET Aspire** para Azure App Service:
+```bash
+cd src/Aspire/MeAjudaAi.AppHost
+dotnet run -- deploy
+```
+Ver [documentação do Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/deployment/azure/aca-deployment) para detalhes.
+
### Standalone Services
**Location**: `compose/standalone/`
diff --git a/infrastructure/SCRIPTS.md b/infrastructure/SCRIPTS.md
new file mode 100644
index 000000000..65b555885
--- /dev/null
+++ b/infrastructure/SCRIPTS.md
@@ -0,0 +1,270 @@
+# 🏗️ Infrastructure Scripts - MeAjudaAi
+
+Scripts para configuração e gerenciamento da infraestrutura local e remota (PostgreSQL, Keycloak, Docker, Azure).
+
+---
+
+## 📋 Índice
+
+- [Database Scripts](#-database-scripts)
+- [Keycloak Scripts](#-keycloak-scripts)
+- [Docker Compose Scripts](#-docker-compose-scripts)
+- [Testing Scripts](#-testing-scripts)
+- [Deployment](#-deployment)
+
+---
+
+## 🗄️ Database Scripts
+
+### **`database/01-init-meajudaai.sh`**
+**Propósito**: Inicialização de schemas PostgreSQL para todos os módulos
+**Quando Usar**: Executado automaticamente pelo Docker Compose no primeiro start
+**Módulos Criados**:
+- `users` - Gerenciamento de usuários
+- `providers` - Prestadores de serviços
+- `service_catalogs` - Catálogo de serviços
+- `documents` - Documentos e verificações
+- `locations` - Cidades e geolocalização
+- `search_providers` - Índice de busca (RediSearch)
+
+**Execução Manual**:
+```bash
+# Conectar ao container PostgreSQL
+docker exec -it postgres psql -U postgres -d meajudaai
+
+# Executar script
+\i /docker-entrypoint-initdb.d/01-init-meajudaai.sh
+```
+
+---
+
+### **`database/create-module.ps1`**
+**Propósito**: Template/helper para criar schema de novo módulo
+**Quando Usar**: Ao adicionar novo módulo ao projeto
+
+**Uso**:
+```powershell
+# Criar schema para novo módulo "Orders"
+.\infrastructure\database\create-module.ps1 -ModuleName "Orders"
+
+# Output: Cria script SQL em database/modules/
+```
+
+**O que gera**:
+- Schema SQL com permissões
+- Tabelas exemplo
+- Extensões necessárias (uuid-ossp)
+
+---
+
+## 🔐 Keycloak Scripts
+
+### **`keycloak/scripts/keycloak-init-dev.sh`**
+**Propósito**: Configuração Keycloak para ambiente Development
+**Quando Usar**: Setup inicial local ou reset de auth
+
+**O que configura**:
+- Realm `meajudaai-dev`
+- Clients: `api-service`, `admin-portal`, `customer-app`
+- Roles padrão: `admin`, `user`, `provider`
+- Usuários de teste
+
+**Execução**:
+```bash
+# Pré-requisito: Keycloak rodando
+docker-compose up -d keycloak
+
+# Executar init
+./infrastructure/keycloak/scripts/keycloak-init-dev.sh
+```
+
+**Variáveis de Ambiente**:
+```bash
+KEYCLOAK_URL=http://localhost:8080
+KEYCLOAK_ADMIN_USER=admin
+KEYCLOAK_ADMIN_PASSWORD=admin
+```
+
+---
+
+### **`keycloak/scripts/keycloak-init-prod.sh`**
+**Propósito**: Configuração Keycloak para ambiente Production
+**Quando Usar**: Deployment em Azure/produção
+
+**Diferenças vs Dev**:
+- ❌ Sem usuários de teste
+- ✅ HTTPS obrigatório
+- ✅ Password policies fortes
+- ✅ Rate limiting configurado
+
+**⚠️ ATENÇÃO**: Este script **NÃO** deve ser executado em produção manualmente. É usado apenas via pipeline CI/CD.
+
+---
+
+## 🐳 Docker Compose Scripts
+
+### **`compose/environments/verify-resources.sh`**
+**Propósito**: Health check de todos os recursos Docker
+**Quando Usar**: Troubleshooting ou validação pós-deploy
+
+**Uso**:
+```bash
+./infrastructure/compose/environments/verify-resources.sh
+```
+
+**Verifica**:
+- ✅ PostgreSQL (port 5432)
+- ✅ Keycloak (port 8080)
+- ✅ Redis (port 6379)
+- ✅ RabbitMQ (port 5672, 15672)
+- ✅ Seq (port 5341)
+
+**Output Exemplo**:
+```
+🔍 Verificando recursos Docker...
+✅ PostgreSQL: Healthy (port 5432)
+✅ Keycloak: Healthy (port 8080)
+✅ Redis: Healthy (port 6379)
+⚠️ RabbitMQ: Not responding (port 5672)
+```
+
+---
+
+### **`compose/standalone/postgres/init/02-custom-setup.sh`**
+**Propósito**: Customizações adicionais PostgreSQL (extensões, configurações)
+**Quando Usar**: Executado automaticamente após `01-init-meajudaai.sh`
+
+**Configurações**:
+- Extensões: PostGIS, pg_trgm, btree_gin
+- Performance tuning para desenvolvimento
+- Logging configurado
+
+---
+
+## 🧪 Testing Scripts
+
+### **`test-database-init.ps1`**
+**Propósito**: Validar que todos os scripts de init executam sem erros
+**Quando Usar**: Após modificar scripts de database ou adicionar novo módulo
+
+**Uso:**
+```powershell
+.\infrastructure\test-database-init.ps1
+```
+
+**O que testa**:
+1. Docker está rodando?
+2. Containers iniciam corretamente?
+3. Scripts SQL executam sem erros?
+4. Schemas foram criados?
+5. Permissões estão corretas?
+
+**Output Exemplo**:
+```
+🧪 Testing Database Initialization Scripts
+
+✅ Docker is running
+✅ Starting containers...
+✅ Executing init scripts...
+✅ Schema 'users' created
+✅ Schema 'providers' created
+...
+✅ All tests passed!
+```
+
+---
+
+## 🚀 Deployment
+
+### **Azure Deployment via Bicep**
+Para deploy em Azure, use diretamente o Azure CLI:
+
+```bash
+# Login no Azure
+az login
+
+# Deploy do resource group e recursos
+az deployment group create \
+ --resource-group meajudaai-prod \
+ --template-file infrastructure/main.bicep \
+ --parameters location=brazilsouth
+```
+
+### **Deploy via .NET Aspire (Recomendado)**
+
+Para ambientes de produção, o deploy é feito via .NET Aspire para Azure App Service:
+
+```bash
+# Deploy via Aspire
+cd src/Aspire/MeAjudaAi.AppHost
+dotnet run -- deploy
+```
+
+Ver [documentação do Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/deployment/azure/aca-deployment) para detalhes.
+
+---
+
+## 📁 Estrutura de Diretórios
+
+```text
+infrastructure/
+├── README.md
+├── SCRIPTS.md (este arquivo)
+├── main.bicep (template Bicep principal)
+├── servicebus.bicep (Azure Service Bus)
+├── database/
+│ ├── 01-init-meajudaai.sh (init PostgreSQL)
+│ └── create-module.ps1 (template novo módulo)
+├── keycloak/
+│ ├── realms/ (configurações Keycloak)
+│ └── scripts/
+│ ├── keycloak-init-dev.sh
+│ └── keycloak-init-prod.sh
+├── compose/
+│ ├── base/ (postgres, keycloak, redis, rabbitmq)
+│ ├── environments/ (development, testing)
+│ │ └── verify-resources.sh
+│ └── standalone/ (postgres-only, keycloak-only)
+├── rabbitmq/ (configs RabbitMQ)
+└── test-database-init.ps1
+```
+
+---
+
+## 🔧 Troubleshooting
+
+### **Problema**: "Schema already exists"
+```bash
+# Solução: Drop e recria
+docker exec -it postgres psql -U postgres -d meajudaai -c "DROP SCHEMA users CASCADE; DROP SCHEMA providers CASCADE;"
+docker-compose restart postgres
+```
+
+### **Problema**: "Permission denied"
+```bash
+# Solução: Dar permissões de execução
+chmod +x infrastructure/**/*.sh
+```
+
+### **Problema**: Keycloak não aceita configuração
+```bash
+# Solução: Reset completo
+docker-compose down -v
+docker-compose up -d keycloak
+# Aguardar 30s para Keycloak inicializar
+./infrastructure/keycloak/scripts/keycloak-init-dev.sh
+```
+
+---
+
+## 📚 Recursos Adicionais
+
+- [Docker Compose Documentation](https://docs.docker.com/compose/)
+- [PostgreSQL Init Scripts](https://hub.docker.com/_/postgres) - ver "Initialization scripts"
+- [Keycloak Admin CLI](https://www.keycloak.org/docs/latest/server_admin/#admin-cli)
+- [Azure Bicep Templates](https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/)
+
+---
+
+**Última Atualização**: 12 Dez 2025
+**Manutenção**: Atualizar ao adicionar novos scripts ou módulos
diff --git a/infrastructure/compose/environments/production.yml b/infrastructure/compose/environments/production.yml
deleted file mode 100644
index 3a3d0f621..000000000
--- a/infrastructure/compose/environments/production.yml
+++ /dev/null
@@ -1,208 +0,0 @@
-# Production-ready docker compose
-# This should be used as a reference - real production
-# Usage: docker compose -f infrastructure/compose/environments/production.yml --env-file .env.prod up -d
-#
-# IMPORTANT: Before running, create Docker secrets:
-# 1. Initialize Docker Swarm: docker swarm init
-# 2. Create Redis secret: echo "your-redis-password" | docker secret create meajudaai_redis_password -
-# 3. Or run: ./setup-secrets.sh
-#
-# Security Features:
-# - All images pinned by SHA256 digest for supply-chain security
-# - Resource limits applied to all services for production hygiene
-# - Docker secrets for sensitive data (Redis password)
-#
-# Resource Allocation:
-# - Main Postgres: 1 CPU, 1GB RAM (primary database)
-# - Keycloak DB: 0.5 CPU, 512MB RAM (identity database)
-# - Keycloak App: 1 CPU, 1GB RAM (identity server)
-# - Redis: 0.5 CPU, 256MB RAM (cache/sessions)
-# - RabbitMQ: 1 CPU, 512MB RAM (message broker)
-# - Total: ~4 CPUs, ~3.25GB RAM
-#
-# Keycloak Configuration Notes:
-# - HTTP is enabled for internal container communication (KC_HTTP_ENABLED=true)
-# - HTTPS enforcement happens at the reverse proxy level (KC_PROXY=edge)
-# - KC_HOSTNAME_STRICT_HTTPS=true ensures external connections use HTTPS
-# - Version pinned for production stability (overrideable via KEYCLOAK_VERSION)
-
----
-services:
- postgres:
- image: postgres:16@sha256:d0f363f8366fbc3f52d172c6e76bc27151c3d643b870e1062b4e8bfe65baf609
- container_name: meajudaai-postgres-prod
- environment:
- POSTGRES_DB: ${POSTGRES_DB:-meajudaai}
- POSTGRES_USER: ${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Missing POSTGRES_PASSWORD environment variable}
- ports:
- - "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
- volumes:
- - postgres_data:/var/lib/postgresql/data
- - ${BACKUPS_DIR:-./backups}:/backups
- restart: unless-stopped
- cpus: "1.0"
- mem_limit: 1g
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
- interval: 30s
- timeout: 10s
- retries: 5
- networks:
- - meajudaai-network
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: "3"
-
- keycloak-db:
- image: postgres:16@sha256:d0f363f8366fbc3f52d172c6e76bc27151c3d643b870e1062b4e8bfe65baf609
- container_name: meajudaai-keycloak-db-prod
- environment:
- POSTGRES_DB: ${KEYCLOAK_DB:-keycloak}
- POSTGRES_USER: ${KEYCLOAK_DB_USER:-keycloak}
- POSTGRES_PASSWORD: ${KEYCLOAK_DB_PASSWORD:?Missing KEYCLOAK_DB_PASSWORD environment variable}
- volumes:
- - keycloak_db_data:/var/lib/postgresql/data
- - ${BACKUPS_DIR:-./backups}:/backups
- restart: unless-stopped
- cpus: "0.5"
- mem_limit: 512m
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U ${KEYCLOAK_DB_USER:-keycloak}"]
- interval: 30s
- timeout: 10s
- retries: 5
- networks:
- - meajudaai-network
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: "3"
-
- keycloak:
- # Pin by digest for supply-chain stability
- # To update: docker pull quay.io/keycloak/keycloak:26.0.2 && docker inspect --format='{{index .RepoDigests 0}}' quay.io/keycloak/keycloak:26.0.2
- image: quay.io/keycloak/keycloak:${KEYCLOAK_VERSION:-26.0.2}@${KEYCLOAK_DIGEST:-sha256:a01c5ebe4b8e4aef91b0e2e22b73f7be40e98b9c3f2c3d8b4e8c1b9f3e2a5d8f}
- container_name: meajudaai-keycloak-prod
- environment:
- KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
- KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:?Missing KEYCLOAK_ADMIN_PASSWORD environment variable}
- KC_DB: postgres
- KC_DB_URL: jdbc:postgresql://keycloak-db:5432/${KEYCLOAK_DB:-keycloak}
- KC_DB_USERNAME: ${KEYCLOAK_DB_USER:-keycloak}
- KC_DB_PASSWORD: ${KEYCLOAK_DB_PASSWORD:?Missing KEYCLOAK_DB_PASSWORD environment variable}
- KC_HOSTNAME: ${KEYCLOAK_HOSTNAME:?Missing KEYCLOAK_HOSTNAME environment variable}
- KC_HOSTNAME_STRICT: true
- KC_HOSTNAME_STRICT_HTTPS: true
- KC_PROXY: edge
- KC_HTTP_ENABLED: true
- KC_HEALTH_ENABLED: true
- KC_METRICS_ENABLED: true
- command: ["start", "--optimized", "--import-realm"]
- ports:
- - "127.0.0.1:${KEYCLOAK_PORT:-8080}:8080"
- volumes:
- - keycloak_data:/opt/keycloak/data
- - ../../keycloak/realms:/opt/keycloak/data/import
- depends_on:
- keycloak-db:
- condition: service_healthy
- restart: unless-stopped
- cpus: "1.0"
- mem_limit: 1g
- healthcheck:
- test: ["CMD-SHELL", "curl -sf http://localhost:8080/health/ready >/dev/null || exit 1"]
- interval: 30s
- timeout: 10s
- retries: 5
- networks:
- - meajudaai-network
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: "3"
-
- redis:
- image: redis:7-alpine
- container_name: meajudaai-redis-prod
- command: ["sh", "-c", "export REDIS_PASSWORD=\"$$(cat /run/secrets/redis_password)\"; redis-server --requirepass \"$$REDIS_PASSWORD\" --appendonly yes"]
- ports:
- - "127.0.0.1:${REDIS_PORT:-6379}:6379"
- volumes:
- - redis_data:/data
- secrets:
- - redis_password
- restart: unless-stopped
- cpus: "0.5"
- mem_limit: 256m
- healthcheck:
- test: ["CMD-SHELL", "export REDIS_PASSWORD=\"$$(cat /run/secrets/redis_password)\"; redis-cli -a \"$$REDIS_PASSWORD\" ping"]
- interval: 30s
- timeout: 10s
- retries: 5
- networks:
- - meajudaai-network
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: "3"
-
- rabbitmq:
- image: rabbitmq:3.13-management-alpine@sha256:1d6e4c5fb7a7b7b3e6d7f8b8c9d5a4b3c2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7
- container_name: meajudaai-rabbitmq-prod
- environment:
- RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:?Missing RABBITMQ_USER environment variable}
- RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:?Missing RABBITMQ_PASS environment variable}
- RABBITMQ_ERLANG_COOKIE: ${RABBITMQ_ERLANG_COOKIE:?Missing RABBITMQ_ERLANG_COOKIE environment variable}
- ports:
- - "127.0.0.1:${RABBITMQ_PORT:-5672}:5672"
- - "127.0.0.1:${RABBITMQ_MANAGEMENT_PORT:-15672}:15672"
- volumes:
- - rabbitmq_data:/var/lib/rabbitmq
- - ../../rabbitmq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
- restart: unless-stopped
- cpus: "1.0"
- mem_limit: 512m
- ulimits:
- nofile:
- soft: 65536
- hard: 65536
- healthcheck:
- test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
- interval: 30s
- timeout: 10s
- retries: 5
- networks:
- - meajudaai-network
- logging:
- driver: "json-file"
- options:
- max-size: "10m"
- max-file: "3"
-
-volumes:
- postgres_data:
- name: meajudaai-postgres-data-prod
- keycloak_db_data:
- name: meajudaai-keycloak-db-data-prod
- keycloak_data:
- name: meajudaai-keycloak-data-prod
- redis_data:
- name: meajudaai-redis-data-prod
- rabbitmq_data:
- name: meajudaai-rabbitmq-data-prod
-
-networks:
- meajudaai-network:
- name: meajudaai-network-prod
- driver: bridge
-
-secrets:
- redis_password:
- external: true
- name: meajudaai_redis_password
\ No newline at end of file
diff --git a/infrastructure/compose/environments/setup-secrets.sh b/infrastructure/compose/environments/setup-secrets.sh
deleted file mode 100644
index da1458b46..000000000
--- a/infrastructure/compose/environments/setup-secrets.sh
+++ /dev/null
@@ -1,120 +0,0 @@
-#!/bin/bash
-set -euo pipefail
-
-# Colors for output
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-NC='\033[0m' # No Color
-
-echo "🔐 Setting up Docker secrets for MeAjudaAi production deployment..."
-
-# Function to validate non-empty input
-validate_input() {
- local input="$1"
- local field_name="$2"
-
- if [[ -z "$input" ]]; then
- echo -e "${RED}❌ Error: $field_name cannot be empty!${NC}" >&2
- return 1
- fi
- return 0
-}
-
-# Function to check if secret exists
-secret_exists() {
- local secret_name="$1"
- docker secret ls --format "{{.Name}}" | grep -q "^${secret_name}$"
-}
-
-# Function to handle existing secret
-handle_existing_secret() {
- local secret_name="$1"
-
- echo -e "${YELLOW}⚠️ Secret '$secret_name' already exists.${NC}"
- echo "What would you like to do?"
- echo "1) Skip creation (keep existing secret)"
- echo "2) Remove and recreate"
- echo "3) Exit script"
-
- while true; do
- read -p "Choose an option (1-3): " choice
- case $choice in
- 1)
- echo -e "${GREEN}✅ Keeping existing secret '$secret_name'${NC}"
- return 1 # Skip creation
- ;;
- 2)
- echo -e "${YELLOW}🗑️ Removing existing secret '$secret_name'...${NC}"
- docker secret rm "$secret_name"
- echo -e "${GREEN}✅ Secret removed successfully${NC}"
- return 0 # Proceed with creation
- ;;
- 3)
- echo -e "${RED}❌ Exiting script...${NC}"
- exit 0
- ;;
- *)
- echo -e "${RED}❌ Invalid choice. Please enter 1, 2, or 3.${NC}"
- ;;
- esac
- done
-}
-
-# Function to create secret with validation
-create_secret_with_validation() {
- local secret_name="$1"
- local prompt_message="$2"
- local password
-
- # Check if secret already exists
- if secret_exists "$secret_name"; then
- if ! handle_existing_secret "$secret_name"; then
- return 0 # Skip creation
- fi
- fi
-
- # Prompt for password with validation
- while true; do
- read -s -p "$prompt_message" password
- echo # New line after hidden input
-
- if validate_input "$password" "password"; then
- break
- else
- echo -e "${RED}❌ Password cannot be empty. Please try again.${NC}"
- fi
- done
-
- # Create the secret
- echo -n "$password" | docker secret create "$secret_name" -
- echo -e "${GREEN}✅ Secret '$secret_name' created successfully${NC}"
-}
-
-# Check if Docker Swarm is initialized
-if ! docker info | grep -q "Swarm: active"; then
- echo -e "${YELLOW}⚠️ Docker Swarm is not active. Initializing Docker Swarm...${NC}"
- docker swarm init
- echo -e "${GREEN}✅ Docker Swarm initialized${NC}"
-fi
-
-echo "📝 Please provide the following credentials for production deployment:"
-echo
-
-# Create all required secrets based on production.yml
-create_secret_with_validation "meajudaai_redis_password" "🔑 Enter Redis password: "
-
-echo
-echo -e "${GREEN}🎉 All secrets created successfully!${NC}"
-echo
-echo "📋 Created secrets:"
-echo " - meajudaai_redis_password"
-echo
-echo -e "${GREEN}✅ You can now run the production stack with:${NC}"
-echo " docker compose -f infrastructure/compose/environments/production.yml --env-file .env.prod up -d"
-echo
-echo "🔍 To verify secrets were created:"
-echo " docker secret ls"
-echo
-echo "🗑️ To remove secrets later:"
-echo " docker secret rm meajudaai_redis_password"
\ No newline at end of file
diff --git a/infrastructure/test-database-init.sh b/infrastructure/test-database-init.sh
deleted file mode 100644
index 22b2b3b52..000000000
--- a/infrastructure/test-database-init.sh
+++ /dev/null
@@ -1,155 +0,0 @@
-#!/bin/bash
-# Test Database Initialization Scripts
-# This script validates that all module database scripts execute successfully
-
-set -e
-
-POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-development123}"
-POSTGRES_USER="${POSTGRES_USER:-postgres}"
-POSTGRES_DB="${POSTGRES_DB:-meajudaai}"
-
-echo "🧪 Testing Database Initialization Scripts"
-echo ""
-
-# Check if Docker is running
-if ! docker ps >/dev/null 2>&1; then
- echo "❌ Docker is not running. Please start Docker."
- exit 1
-fi
-
-# Export environment variables
-export POSTGRES_PASSWORD
-export POSTGRES_USER
-export POSTGRES_DB
-
-# Navigate to infrastructure/compose directory
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-COMPOSE_DIR="$SCRIPT_DIR/compose/base"
-
-if [ ! -d "$COMPOSE_DIR" ]; then
- echo "❌ Compose directory not found: $COMPOSE_DIR"
- exit 1
-fi
-
-cd "$COMPOSE_DIR"
-
-echo "🐳 Starting PostgreSQL container with initialization scripts..."
-echo ""
-
-# Stop and remove existing container
-docker compose -f postgres.yml down -v 2>/dev/null || true
-
-# Start container
-docker compose -f postgres.yml up -d
-
-# Wait for PostgreSQL to be ready
-echo "⏳ Waiting for PostgreSQL to be ready..."
-MAX_ATTEMPTS=30
-ATTEMPT=0
-READY=false
-
-while [ $ATTEMPT -lt $MAX_ATTEMPTS ] && [ "$READY" != "true" ]; do
- ATTEMPT=$((ATTEMPT + 1))
- sleep 2
-
- HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' meajudaai-postgres 2>/dev/null || echo "unknown")
- if [ "$HEALTH_STATUS" = "healthy" ]; then
- READY=true
- echo "✅ PostgreSQL is ready!"
- else
- echo " Attempt $ATTEMPT/$MAX_ATTEMPTS - Status: $HEALTH_STATUS"
- fi
-done
-
-if [ "$READY" != "true" ]; then
- echo "❌ PostgreSQL failed to start within timeout period"
- docker logs meajudaai-postgres
- exit 1
-fi
-
-echo ""
-echo "🔍 Verifying database schemas..."
-
-# Track validation errors
-has_errors=false
-
-# Test schemas
-SCHEMAS=("users" "providers" "documents" "search" "location" "catalogs" "hangfire" "meajudaai_app")
-
-for schema in "${SCHEMAS[@]}"; do
- # Use double-dollar quoting to safely handle identifiers
- QUERY="SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = \$\$${schema}\$\$);"
- RESULT=$(docker exec meajudaai-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c "$QUERY" | tr -d '[:space:]')
-
- if [ "$RESULT" = "t" ]; then
- echo " ✅ Schema '$schema' created successfully"
- else
- echo " ❌ Schema '$schema' NOT found"
- has_errors=true
- fi
-done
-
-echo ""
-echo "🔍 Verifying database roles..."
-
-# Test roles
-ROLES=(
- "users_role" "users_owner"
- "providers_role" "providers_owner"
- "documents_role" "documents_owner"
- "search_role" "search_owner"
- "location_role" "location_owner"
- "catalogs_role" "catalogs_owner"
- "hangfire_role"
- "meajudaai_app_role" "meajudaai_app_owner"
-)
-
-for role in "${ROLES[@]}"; do
- # Use double-dollar quoting to safely handle identifiers
- QUERY="SELECT EXISTS(SELECT 1 FROM pg_roles WHERE rolname = \$\$${role}\$\$);"
- RESULT=$(docker exec meajudaai-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c "$QUERY" | tr -d '[:space:]')
-
- if [ "$RESULT" = "t" ]; then
- echo " ✅ Role '$role' created successfully"
- else
- echo " ❌ Role '$role' NOT found"
- has_errors=true
- fi
-done
-
-echo ""
-echo "🔍 Verifying PostGIS extension..."
-
-# Use double-dollar quoting to safely handle identifier
-QUERY="SELECT EXISTS(SELECT 1 FROM pg_extension WHERE extname = \$\$postgis\$\$);"
-RESULT=$(docker exec meajudaai-postgres psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c "$QUERY" | tr -d '[:space:]')
-
-if [ "$RESULT" = "t" ]; then
- echo " ✅ PostGIS extension enabled"
-else
- echo " ❌ PostGIS extension NOT enabled"
- has_errors=true
-fi
-
-echo ""
-echo "📊 Database initialization logs:"
-echo ""
-docker logs meajudaai-postgres 2>&1 | grep -E "Initializing|Setting up|completed" || \
- (echo "⚠️ No matching initialization logs found. Full output:" && docker logs meajudaai-postgres 2>&1)
-
-echo ""
-
-if [ "$has_errors" = "true" ]; then
- echo "❌ Database validation failed! Some schemas, roles, or extensions are missing."
- echo ""
- exit 1
-fi
-
-echo "✅ Database validation completed!"
-echo ""
-echo "💡 To connect to the database:"
-echo " docker exec -it meajudaai-postgres psql -U $POSTGRES_USER -d $POSTGRES_DB"
-echo ""
-echo "💡 To stop the container:"
-echo " docker compose -f $COMPOSE_DIR/postgres.yml down"
-echo ""
diff --git a/scripts/README.md b/scripts/README.md
index 5be778350..280c3d10a 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -1,408 +1,123 @@
-# 🛠️ MeAjudaAi Scripts - Guia de Uso
+# 🛠️ Scripts - MeAjudaAi
-Este diretório contém todos os scripts essenciais para desenvolvimento, teste e deploy da aplicação MeAjudaAi. Os scripts foram consolidados e padronizados para maior simplicidade e eficiência.
-
-## 📋 **Scripts Disponíveis**
-
-### 🚀 **dev.sh** - Desenvolvimento Local
-Script principal para desenvolvimento local da aplicação.
-
-```bash
-# Menu interativo
-./scripts/dev.sh
-
-# Execução direta
-./scripts/dev.sh --simple # Modo simples (sem Azure)
-./scripts/dev.sh --test-only # Apenas testes
-./scripts/dev.sh --build-only # Apenas build
-./scripts/dev.sh --verbose # Modo verboso
-```
-
-**Funcionalidades:**
-- ✅ Verificação automática de dependências
-- 🔨 Build e compilação da solução
-- 🧪 Execução de testes
-- 🐳 Configuração Docker automática
-- ☁️ Integração com Azure (opcional)
-- 📱 Menu interativo para facilitar uso
+Scripts PowerShell essenciais para desenvolvimento e operações da aplicação.
---
-### 🧪 **test.sh** - Execução de Testes
-Script otimizado para execução abrangente de testes.
-
-```bash
-# Todos os testes
-./scripts/test.sh
+## 📋 Scripts Disponíveis
-# Testes específicos
-./scripts/test.sh --unit # Apenas unitários
-./scripts/test.sh --integration # Apenas integração
-./scripts/test.sh --e2e # Apenas E2E
+### 🗄️ Banco de Dados e Migrations
-# Com otimizações
-./scripts/test.sh --fast # Modo otimizado (70% mais rápido)
-./scripts/test.sh --coverage # Com relatório de cobertura
-./scripts/test.sh --parallel # Execução paralela
-```
-
-**Funcionalidades:**
-- 🎯 Filtros por tipo de teste (unitário, integração, E2E)
-- ⚡ Modo otimizado com 70% de melhoria de performance
-- 📊 Relatórios de cobertura com HTML
-- 🔄 Execução paralela
-- 📝 Logs detalhados com diferentes níveis
-
----
+#### `ef-migrate.ps1` - Entity Framework Migrations
+**Uso:**
+```powershell
+# Aplicar migrações em todos os módulos
+.\scripts\ef-migrate.ps1
-### 🌐 **deploy.sh** - Deploy Azure
-Script para deploy automatizado da infraestrutura Azure.
+# Aplicar em módulo específico
+.\scripts\ef-migrate.ps1 -Module Providers
-```bash
-# Deploy básico
-./scripts/deploy.sh dev brazilsouth
+# Adicionar nova migração
+.\scripts\ef-migrate.ps1 -Command add -Module Users -MigrationName "AddNewField"
-# Deploy com opções
-./scripts/deploy.sh prod brazilsouth --verbose
-./scripts/deploy.sh production eastus --what-if # Simular mudanças
-./scripts/deploy.sh dev brazilsouth --dry-run # Simulação completa
+# Ver status das migrações
+.\scripts\ef-migrate.ps1 -Command status
```
**Funcionalidades:**
-- 🌍 Suporte a múltiplos ambientes (dev, prod)
-- ✅ Validação de templates Bicep
-- 🔍 Análise what-if antes do deploy
-- 📋 Relatórios detalhados de outputs
-- 🔒 Gestão segura de secrets e connection strings
+- Aplica migrações usando `dotnet ef`
+- Suporta múltiplos módulos (Users, Providers)
+- Comandos: migrate, add, remove, status
+- Configuração via variáveis de ambiente
---
-### ⚙️ **setup.sh** - Configuração Inicial
-Script para onboarding de novos desenvolvedores.
+#### `migrate-all.ps1` - Migrations para Todos os Módulos
+**Uso:**
+```powershell
+# Aplicar todas as migrações
+.\scripts\migrate-all.ps1
-```bash
-# Setup completo
-./scripts/setup.sh
+# Ver status
+.\scripts\migrate-all.ps1 -Command status
-# Setup customizado
-./scripts/setup.sh --dev-only # Apenas ferramentas de dev
-./scripts/setup.sh --no-docker # Sem Docker
-./scripts/setup.sh --no-azure # Sem Azure CLI
-./scripts/setup.sh --force # Forçar reinstalação
+# Resetar bancos (CUIDADO!)
+.\scripts\migrate-all.ps1 -Command reset
```
**Funcionalidades:**
-- 🔍 Verificação automática de dependências
-- 📦 Instalação guiada de ferramentas
-- 🎯 Configuração do ambiente de projeto
-- 📚 Instruções específicas por SO
-- ✅ Validação de configuração
+- Descobre automaticamente todos os DbContexts
+- Executa migrações em sequência
+- Comandos: migrate, create, reset, status
---
-### 📋 **export-openapi.ps1** - Gerador OpenAPI
-Script para gerar especificação OpenAPI para clientes REST.
+### 📄 API e Documentação
-```bash
-# Gerar especificação padrão (api-spec.json no diretório api)
-./scripts/export-openapi.ps1
-
-# Especificar arquivo de saída (sempre relativo à raiz do projeto)
-./scripts/export-openapi.ps1 -OutputPath "minha-api.json"
-./scripts/export-openapi.ps1 -OutputPath "docs/api-spec.json"
+#### `export-openapi.ps1` - Export OpenAPI Specification
+**Uso:**
+```powershell
+# Export para arquivo padrão
+.\scripts\export-openapi.ps1
-# Ajuda
-./scripts/export-openapi.ps1 -Help
+# Export para arquivo específico
+.\scripts\export-openapi.ps1 -OutputPath "api/frontend-api.json"
```
**Funcionalidades:**
-- 🚀 **Funciona offline** (não precisa rodar aplicação)
-- 📋 **Health checks incluídos** (health, ready, live)
-- 🎯 **Compatível com todos os clientes** (APIDog, Postman, Insomnia, Bruno, Thunder Client)
-- 🔒 **Arquivos não versionados** (incluídos no .gitignore)
-- ✨ **Schemas com exemplos** realistas para desenvolvimento
-
-**Uso típico:**
-```bash
-# Gerar no diretório api e importar no cliente de API preferido
-./scripts/export-openapi.ps1 -OutputPath "api/api-spec.json"
-# → Arquivo criado em: C:\Code\MeAjudaAi\api\api-spec.json
-# → Importar arquivo em APIDog/Postman/Insomnia
-```
-
-**📁 Local de saída:** Arquivos sempre são criados na **raiz do projeto**, não na pasta `scripts`.
+- Exporta especificação OpenAPI da API
+- Formato JSON compatível com ferramentas
+- Usado para gerar cliente HTTP/Bruno Collections
---
-### ⚡ **optimize.sh** - Otimizações de Performance
-Script para aplicar otimizações de performance em testes.
-
-```bash
-# Aplicar otimizações
-./scripts/optimize.sh
-
-# Aplicar e testar
-./scripts/optimize.sh --test # Aplica e executa teste de performance
-
-# Usar no shell atual
-source ./scripts/optimize.sh # Mantém variáveis no shell
-
-# Restaurar configurações
-./scripts/optimize.sh --reset # Remove otimizações
-```
-
-**Funcionalidades:**
-- 🚀 70% de melhoria na performance dos testes
-- 🐳 Otimizações específicas para Docker/TestContainers
-- ⚙️ Configurações otimizadas do .NET Runtime
-- 🐘 Configurações de PostgreSQL para testes
-- 🔄 Sistema de backup/restore de configurações
-
----
+### 🌱 Seed de Dados
-### 🛠️ **utils.sh** - Utilidades Compartilhadas
-Biblioteca de funções compartilhadas entre scripts.
+#### `seed-dev-data.ps1` - Seed Dados de Desenvolvimento
+**Uso:**
+```powershell
+# Quando executar API diretamente (dotnet run) - usa default http://localhost:5000
+.\scripts\seed-dev-data.ps1
-```bash
-# Carregar no script
-source ./scripts/utils.sh
+# Quando usar Aspire orchestration - override para portas Aspire
+.\scripts\seed-dev-data.ps1 -ApiBaseUrl "https://localhost:7524"
+# ou
+.\scripts\seed-dev-data.ps1 -ApiBaseUrl "http://localhost:5545"
-# Usar funções
-print_info "Mensagem informativa"
-check_essential_dependencies
-docker_cleanup
+# Seed para Staging
+.\scripts\seed-dev-data.ps1 -Environment Staging
```
**Funcionalidades:**
-- 📝 Sistema de logging padronizado
-- ✅ Validações e verificações comuns
-- 🖥️ Detecção automática de SO
-- 🐳 Helpers para Docker
-- ⚙️ Helpers para .NET
-- ⏱️ Medição de performance
-
----
-
-## 🎯 **Fluxo de Uso Recomendado**
-
-### **Para Novos Desenvolvedores:**
-```bash
-1. ./scripts/setup.sh # Configurar ambiente
-2. ./scripts/dev.sh # Executar aplicação
-3. ./scripts/test.sh # Validar com testes
-```
-
-### **Desenvolvimento Diário:**
-```bash
-./scripts/dev.sh --simple # Desenvolvimento local rápido
-./scripts/test.sh --fast # Testes otimizados
-```
-
-### **Deploy para Produção:**
-```bash
-./scripts/test.sh # Validar todos os testes
-./scripts/deploy.sh prod brazilsouth --what-if # Simular deploy
-./scripts/deploy.sh prod brazilsouth # Deploy real
-```
-
-### **Otimização de Performance:**
-```bash
-./scripts/optimize.sh --test # Aplicar e testar otimizações
-./scripts/test.sh --fast # Usar testes otimizados
-```
-
----
-
-## 🔧 **Configurações Globais**
-
-### **Variáveis de Ambiente:**
-```bash
-# Nível de log (1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE)
-export MEAJUDAAI_LOG_LEVEL=3
-
-# Desabilitar auto-inicialização do utils
-export MEAJUDAAI_UTILS_AUTO_INIT=false
-
-# Configurações de otimização
-export MEAJUDAAI_FAST_MODE=true
-```
-
-### **Arquivo de Configuração:**
-Crie `.meajudaai.config` na raiz do projeto para configurações persistentes:
-
-```bash
-DEFAULT_ENVIRONMENT=dev
-DEFAULT_LOCATION=brazilsouth
-ENABLE_OPTIMIZATIONS=true
-SKIP_DOCKER_CHECK=false
-```
-
----
-
-## 📊 **Comparação: Antes vs Depois**
-
-| Aspecto | Antes (12+ scripts) | Depois (6 scripts) | Melhoria |
-|---------|---------------------|-------------------|----------|
-| **Scripts totais** | 12+ | 6 | 50% redução |
-| **Documentação** | Inconsistente | Padronizada | 100% melhoria |
-| **Duplicação** | Muita | Zero | Eliminada |
-| **Onboarding** | ~30 min | ~5 min | 83% redução |
-| **Performance testes** | ~25s | ~8s | 70% melhoria |
-| **Manutenção** | Complexa | Simples | 80% redução |
-
----
-
-## 🚨 **Migração dos Scripts Antigos**
-
-Os scripts antigos foram movidos para `scripts/deprecated/` para compatibilidade temporária:
-
-```bash
-# Scripts deprecados (usar novos equivalentes)
-scripts/deprecated/run-local.sh → scripts/dev.sh
-scripts/deprecated/test.sh → scripts/test.sh
-scripts/deprecated/infrastructure/deploy.sh → scripts/deploy.sh
-scripts/deprecated/optimize-tests.sh → scripts/optimize.sh
-```
-
-**⚠️ Atenção:** Os scripts em `deprecated/` serão removidos em versões futuras. Migre para os novos scripts.
-
----
-
-## 🆘 **Resolução de Problemas**
-
-### **Script não executa:**
-```bash
-# Dar permissão de execução
-chmod +x scripts/*.sh
-
-# Verificar se está na raiz do projeto
-pwd # Deve mostrar o diretório MeAjudaAi
-```
-
-### **Dependências não encontradas:**
-```bash
-# Executar setup
-./scripts/setup.sh --verbose
-
-# Verificar dependências manualmente
-./scripts/dev.sh --help
-```
-
-### **Performance lenta nos testes:**
-```bash
-# Aplicar otimizações
-./scripts/optimize.sh --test
-
-# Usar modo rápido
-./scripts/test.sh --fast
-```
-
-### **Problemas com Docker:**
-```bash
-# Verificar status
-docker info
-
-# Limpar containers
-source ./scripts/utils.sh
-docker_cleanup
-```
+- Popula categorias de serviços
+- Cria serviços básicos
+- Adiciona cidades permitidas
+- Cria usuários de teste
+- Gera providers de exemplo
+
+**Configuração:**
+- Variável `API_BASE_URL`:
+ - **Default `http://localhost:5000`** - use quando executar API diretamente via `dotnet run`
+ - **Override com `-ApiBaseUrl`** - necessário quando usar Aspire orchestration (portas dinâmicas como `https://localhost:7524` ou `http://localhost:5545`)
+- Suporta ambientes: Development, Staging
---
-## 📚 **Recursos Adicionais**
-
-- **Documentação do projeto:** [README.md](../README.md)
-- **Documentação da infraestrutura:** [infrastructure/README.md](../infrastructure/README.md)
-- **Guia de CI/CD:** [docs/CI-CD-Setup.md](../docs/CI-CD-Setup.md)
-- **Análise de scripts:** [docs/Scripts-Analysis.md](../docs/Scripts-Analysis.md)
-
----
+## 📍 Outros Scripts no Projeto
-## 🤝 **Contribuição**
+### Infrastructure Scripts
+Localizados em `infrastructure/` - documentados em [infrastructure/SCRIPTS.md](../infrastructure/SCRIPTS.md)
-Para adicionar novos scripts ou modificar existentes:
+### Automation Scripts
+Localizados em `automation/` - documentados em [automation/README.md](../automation/README.md)
-1. **Seguir padrão de documentação** (ver cabeçalho dos scripts existentes)
-2. **Usar funções do utils.sh** sempre que possível
-3. **Adicionar testes** para scripts críticos
-4. **Atualizar este README** com as mudanças
+### Build Scripts
+Localizados em `build/` - documentados em [build/README.md](../build/README.md)
---
-## 🔧 Ferramentas de Migração de Banco de Dados
-
-### ef-migrate.ps1 - **Recomendado**
-
-Gerencia migrações usando comandos `dotnet ef` diretamente.
-
-**Windows (PowerShell):**
-```powershell
-# Aplicar todas as migrações para todos os módulos
-.\scripts\ef-migrate.ps1
-
-# Aplicar migrações para um módulo específico
-.\scripts\ef-migrate.ps1 -Module Providers
-
-# Ver status das migrações
-.\scripts\ef-migrate.ps1 -Command status
-
-# Adicionar nova migração
-.\scripts\ef-migrate.ps1 -Command add -Module Users -MigrationName "AddNewUserField"
-```
-
-**Unix/Linux/macOS (PowerShell Core):**
-```bash
-# Aplicar todas as migrações para todos os módulos
-./scripts/ef-migrate.ps1
-
-# Aplicar migrações para um módulo específico
-./scripts/ef-migrate.ps1 -Module Providers
-
-# Ver status das migrações
-./scripts/ef-migrate.ps1 -Command status
-
-# Adicionar nova migração
-./scripts/ef-migrate.ps1 -Command add -Module Users -MigrationName "AddNewUserField"
-```
-
-**📋 Requisitos:**
-- PowerShell 7+ (para Unix/Linux/macOS: instale via `snap install powershell --classic` ou similar)
-- Variáveis de ambiente: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
-
-### migrate-all.ps1 - **Avançado**
-
-Ferramenta customizada que descobre automaticamente todos os DbContexts.
-
-**Windows (PowerShell):**
-```powershell
-# Aplicar migrações
-.\scripts\migrate-all.ps1
-
-# Ver status detalhado
-.\scripts\migrate-all.ps1 -Command status
-
-# Resetar bancos (CUIDADO!)
-.\scripts\migrate-all.ps1 -Command reset
-```
-
-**Unix/Linux/macOS (PowerShell Core):**
-```bash
-# Aplicar migrações
-./scripts/migrate-all.ps1
-
-# Ver status detalhado
-./scripts/migrate-all.ps1 -Command status
-
-# Resetar bancos (CUIDADO!)
-./scripts/migrate-all.ps1 -Command reset
-```
-
-**Módulos Suportados:**
-- Users (`meajudaai_users`)
-- Providers (`meajudaai_providers`)
-- Services (`meajudaai_services`)
-- Orders (`meajudaai_orders`)
-
----
+## 📊 Resumo
-**💡 Dica:** Use `./scripts/[script].sh --help` para ver todas as opções disponíveis de cada script!
\ No newline at end of file
+- **Total de scripts:** 4 PowerShell essenciais
+- **Foco:** Migrations, seed de dados, export de API
+- **Filosofia:** Apenas scripts com utilidade clara e automação
diff --git a/scripts/aggregate-coverage-local.ps1 b/scripts/aggregate-coverage-local.ps1
deleted file mode 100644
index feddfe9bd..000000000
--- a/scripts/aggregate-coverage-local.ps1
+++ /dev/null
@@ -1,111 +0,0 @@
-# Script para rodar TODOS os testes (Unit + Integration + E2E) e agregar coverage
-# Simula exatamente o que o GitHub Actions faz
-
-Write-Host "📊 Running ALL tests with coverage (Unit + Integration + E2E)..." -ForegroundColor Cyan
-Write-Host "⚠️ NOTE: Integration/E2E tests require Docker containers running!" -ForegroundColor Yellow
-Write-Host ""
-
-# Limpar coverage anterior
-Remove-Item -Recurse -Force coverage -ErrorAction SilentlyContinue
-
-# Criar runsettings com mesmos filtros do GitHub
-$runsettingsPath = "coverage.runsettings"
-$excludeByFile = "**/*OpenApi*.generated.cs,**/System.Runtime.CompilerServices*.cs,**/*RegexGenerator.g.cs"
-$excludeFilter = "[*.Tests]*,[*.Tests.*]*,[*Test*]*,[testhost]*,[xunit*]*"
-$excludeByAttribute = "Obsolete,GeneratedCode,CompilerGenerated"
-
-@"
-
-
-
-
-
-
- opencover
- $excludeFilter
- $excludeByFile
- $excludeByAttribute
-
-
-
-
-
-"@ | Out-File -FilePath $runsettingsPath -Encoding utf8
-
-Write-Host "✅ Created runsettings with GitHub filters" -ForegroundColor Green
-Write-Host " - Excludes: .Tests assemblies, compiler-generated code" -ForegroundColor Gray
-Write-Host ""
-
-# Rodar TODOS os testes (Unit + Integration + E2E) com coverage
-Write-Host "🧪 Running ALL tests..." -ForegroundColor Cyan
-dotnet test MeAjudaAi.sln `
- --collect:"XPlat Code Coverage" `
- --results-directory ./coverage `
- --settings $runsettingsPath `
- --configuration Debug `
- --no-restore
-
-if ($LASTEXITCODE -ne 0) {
- Write-Host "⚠️ Some tests failed, but continuing with coverage aggregation..." -ForegroundColor Yellow
-}
-
-Write-Host ""
-
-# Instalar ReportGenerator se necessário
-$reportGen = Get-Command reportgenerator -ErrorAction SilentlyContinue
-if (-not $reportGen) {
- Write-Host "Installing ReportGenerator..." -ForegroundColor Yellow
- dotnet tool install --global dotnet-reportgenerator-globaltool
-}
-
-# Encontrar todos os arquivos de coverage
-Write-Host "`n🔍 Finding coverage files..." -ForegroundColor Cyan
-$coverageFiles = Get-ChildItem -Path "coverage" -Recurse -Filter "*.xml" | Where-Object { $_.Name -like "*coverage*" -or $_.Name -like "*.cobertura.xml" -or $_.Name -like "*.opencover.xml" }
-
-if ($coverageFiles.Count -eq 0) {
- Write-Host "❌ No coverage files found!" -ForegroundColor Red
- exit 1
-}
-
-Write-Host "Found $($coverageFiles.Count) coverage files:" -ForegroundColor Green
-$coverageFiles | ForEach-Object { Write-Host " ✅ $($_.FullName)" -ForegroundColor Gray }
-
-# Gerar relatório agregado (mesmos filtros do GitHub)
-Write-Host "`n🔗 Generating aggregated report..." -ForegroundColor Cyan
-
-$includeFilter = "+MeAjudaAi.Modules.Users.*;+MeAjudaAi.Modules.Providers.*;+MeAjudaAi.Modules.Documents.*;+MeAjudaAi.Modules.ServiceCatalogs.*;+MeAjudaAi.Modules.Locations.*;+MeAjudaAi.Modules.SearchProviders.*;+MeAjudaAi.Shared*;+MeAjudaAi.ApiService*"
-$excludeFilter = "-[*.Tests]*;-[*.Tests.*]*;-[*Test*]*;-[testhost]*;-[xunit*]*;-[*]*.Migrations.*;-[*]*.Contracts;-[*]*.Database"
-
-# Criar lista de reports
-$reports = ($coverageFiles | ForEach-Object { $_.FullName }) -join ";"
-
-# Criar diretório de saída
-New-Item -ItemType Directory -Force -Path "coverage\aggregate" | Out-Null
-
-# Executar ReportGenerator
-reportgenerator `
- "-reports:$reports" `
- "-targetdir:coverage\aggregate" `
- "-reporttypes:Cobertura;HtmlInline_AzurePipelines" `
- "-assemblyfilters:$includeFilter" `
- "-classfilters:$excludeFilter"
-
-if (Test-Path "coverage\aggregate\Cobertura.xml") {
- Write-Host "`n✅ Aggregated coverage report generated!" -ForegroundColor Green
- Write-Host " Location: coverage\aggregate\Cobertura.xml" -ForegroundColor Gray
-
- # Extrair porcentagem
- $xml = [xml](Get-Content "coverage\aggregate\Cobertura.xml")
- $lineRate = [double]$xml.coverage.'line-rate'
- $percentage = [math]::Round($lineRate * 100, 2)
-
- Write-Host "`n📈 Combined Line Coverage: $percentage%" -ForegroundColor Cyan
-
- # Abrir HTML
- if (Test-Path "coverage\aggregate\index.html") {
- Write-Host "`n🌐 Opening HTML report..." -ForegroundColor Cyan
- Start-Process "coverage\aggregate\index.html"
- }
-} else {
- Write-Host "`n❌ Failed to generate aggregated coverage report!" -ForegroundColor Red
-}
diff --git a/scripts/analyze-coverage-detailed.ps1 b/scripts/analyze-coverage-detailed.ps1
deleted file mode 100644
index deb74d8a6..000000000
--- a/scripts/analyze-coverage-detailed.ps1
+++ /dev/null
@@ -1,269 +0,0 @@
-#!/usr/bin/env pwsh
-<#
-.SYNOPSIS
- Análise detalhada de code coverage por camada e módulo
-
-.DESCRIPTION
- Gera análise detalhada mostrando classes com baixo coverage
- e identifica alvos de alta prioridade para melhorias
-
-.EXAMPLE
- .\analyze-coverage-detailed.ps1
-#>
-
-param(
- [int]$TopN = 20,
- [double]$LowCoverageThreshold = 30.0
-)
-
-$ErrorActionPreference = "Stop"
-
-Write-Host "🔍 Análise Detalhada de Code Coverage" -ForegroundColor Cyan
-Write-Host ""
-
-# Verificar se existe relatório
-if (-not (Test-Path "CoverageReport\Summary.json")) {
- Write-Host "❌ Relatório de coverage não encontrado!" -ForegroundColor Red
- Write-Host "Execute: dotnet test --collect:'XPlat Code Coverage'" -ForegroundColor Yellow
- exit 1
-}
-
-# Ler summary
-$summary = Get-Content "CoverageReport\Summary.json" | ConvertFrom-Json
-
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "📊 COVERAGE GERAL" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-Write-Host " Linhas: $($summary.summary.linecoverage)% ($($summary.summary.coveredlines)/$($summary.summary.coverablelines))" -ForegroundColor White
-Write-Host " Branches: $($summary.summary.branchcoverage)% ($($summary.summary.coveredbranches)/$($summary.summary.totalbranches))" -ForegroundColor White
-Write-Host " Métodos: $($summary.summary.methodcoverage)% ($($summary.summary.coveredmethods)/$($summary.summary.totalmethods))" -ForegroundColor White
-Write-Host ""
-
-# Análise por camada
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "🏗️ COVERAGE POR CAMADA" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-$layers = @{
- "Domain" = @()
- "Application" = @()
- "Infrastructure" = @()
- "API" = @()
- "Tests" = @()
- "Other" = @()
-}
-
-foreach ($assembly in $summary.coverage.assemblies) {
- if ($assembly.name -match "Generated|CompilerServices") { continue }
-
- $layer = "Other"
- if ($assembly.name -match "\.Domain$") { $layer = "Domain" }
- elseif ($assembly.name -match "\.Application$") { $layer = "Application" }
- elseif ($assembly.name -match "\.Infrastructure$") { $layer = "Infrastructure" }
- elseif ($assembly.name -match "\.API$") { $layer = "API" }
- elseif ($assembly.name -match "Tests") { $layer = "Tests" }
-
- $layers[$layer] += $assembly
-}
-
-foreach ($layerName in @("Domain", "Application", "Infrastructure", "API", "Other")) {
- $layerAssemblies = $layers[$layerName]
- if ($layerAssemblies.Count -eq 0) { continue }
-
- $totalLines = ($layerAssemblies | Measure-Object -Property coverablelines -Sum).Sum
- $coveredLines = ($layerAssemblies | Measure-Object -Property coveredlines -Sum).Sum
- $avgCoverage = if ($totalLines -gt 0) { [Math]::Round(($coveredLines / $totalLines) * 100, 1) } else { 0 }
-
- $color = if ($avgCoverage -ge 70) { "Green" }
- elseif ($avgCoverage -ge 50) { "Yellow" }
- elseif ($avgCoverage -ge 30) { "DarkYellow" }
- else { "Red" }
-
- Write-Host " $($layerName.PadRight(15)) " -NoNewline -ForegroundColor Gray
- Write-Host "$avgCoverage% " -NoNewline -ForegroundColor $color
- Write-Host "($coveredLines/$totalLines linhas, $($layerAssemblies.Count) assemblies)" -ForegroundColor DarkGray
-}
-
-Write-Host ""
-
-# Top N classes com BAIXO coverage
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "🎯 TOP $TopN CLASSES COM BAIXO COVERAGE (<$LowCoverageThreshold%)" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-$lowCoverageClasses = @()
-
-foreach ($assembly in $summary.coverage.assemblies) {
- if ($assembly.name -match "Generated|CompilerServices|Tests") { continue }
-
- foreach ($class in $assembly.classesinassembly) {
- if ($class.coverage -lt $LowCoverageThreshold -and $class.coverablelines -gt 20) {
- $lowCoverageClasses += [PSCustomObject]@{
- Assembly = $assembly.name -replace "MeAjudaAi\.", ""
- Class = $class.name -replace "MeAjudaAi\.", ""
- Coverage = $class.coverage
- Lines = $class.coverablelines
- UncoveredLines = $class.coverablelines - $class.coveredlines
- Impact = if ($summary.summary.coverablelines -eq 0) { 0 } else { ($class.coverablelines - $class.coveredlines) / $summary.summary.coverablelines * 100 }
- }
- }
- }
-}
-
-$topLowCoverage = $lowCoverageClasses |
- Sort-Object -Property UncoveredLines -Descending |
- Select-Object -First $TopN
-
-$count = 1
-foreach ($item in $topLowCoverage) {
- $className = $item.Class
- if ($className.Length -gt 55) {
- $className = $className.Substring(0, 52) + "..."
- }
-
- $color = if ($item.Coverage -eq 0) { "Red" }
- elseif ($item.Coverage -lt 10) { "DarkRed" }
- elseif ($item.Coverage -lt 20) { "DarkYellow" }
- else { "Yellow" }
-
- Write-Host " $($count.ToString().PadLeft(2)). " -NoNewline -ForegroundColor Gray
- Write-Host "$className" -ForegroundColor White
- Write-Host " Coverage: " -NoNewline -ForegroundColor DarkGray
- Write-Host "$($item.Coverage)% " -NoNewline -ForegroundColor $color
- Write-Host "| Linhas: $($item.Lines) | Não cobertas: $($item.UncoveredLines) " -NoNewline -ForegroundColor DarkGray
- Write-Host "(+$([Math]::Round($item.Impact, 2))pp)" -ForegroundColor Magenta
- Write-Host " Módulo: $($item.Assembly)" -ForegroundColor DarkGray
- Write-Host ""
-
- $count++
-}
-
-# Análise por módulo
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "📦 COVERAGE POR MÓDULO" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-$modules = @{}
-
-foreach ($assembly in $summary.coverage.assemblies) {
- if ($assembly.name -match "Generated|CompilerServices|Tests|ApiService|AppHost|ServiceDefaults|Shared$") { continue }
-
- # Extrair nome do módulo
- if ($assembly.name -match "Modules\.(\w+)\.") {
- $moduleName = $Matches[1]
-
- if (-not $modules.ContainsKey($moduleName)) {
- $modules[$moduleName] = @{
- Assemblies = @()
- TotalLines = 0
- CoveredLines = 0
- }
- }
-
- $modules[$moduleName].Assemblies += $assembly
- $modules[$moduleName].TotalLines += $assembly.coverablelines
- $modules[$moduleName].CoveredLines += $assembly.coveredlines
- }
-}
-
-$moduleStats = $modules.GetEnumerator() | ForEach-Object {
- $avgCoverage = if ($_.Value.TotalLines -gt 0) {
- [Math]::Round(($_.Value.CoveredLines / $_.Value.TotalLines) * 100, 1)
- } else { 0 }
-
- [PSCustomObject]@{
- Module = $_.Key
- Coverage = $avgCoverage
- Lines = $_.Value.TotalLines
- Uncovered = $_.Value.TotalLines - $_.Value.CoveredLines
- AssemblyCount = $_.Value.Assemblies.Count
- }
-} | Sort-Object -Property Coverage
-
-foreach ($stat in $moduleStats) {
- $color = if ($stat.Coverage -ge 70) { "Green" }
- elseif ($stat.Coverage -ge 50) { "Yellow" }
- elseif ($stat.Coverage -ge 30) { "DarkYellow" }
- else { "Red" }
-
- $modulePadded = $stat.Module.PadRight(20)
- $coveragePadded = "$($stat.Coverage)%".PadLeft(6)
-
- Write-Host " $modulePadded " -NoNewline -ForegroundColor Gray
- Write-Host "$coveragePadded " -NoNewline -ForegroundColor $color
- Write-Host "($($stat.Lines) linhas, +$($stat.Uncovered) não cobertas)" -ForegroundColor DarkGray
-}
-
-Write-Host ""
-
-# Recomendações
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "💡 RECOMENDAÇÕES PRIORITÁRIAS" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-# Identificar handlers sem coverage
-$uncoveredHandlers = @()
-foreach ($assembly in $summary.coverage.assemblies) {
- if ($assembly.name -notmatch "Application" -or $assembly.name -match "Tests") { continue }
-
- foreach ($class in $assembly.classesinassembly) {
- if ($class.name -match "Handler$" -and $class.coverage -eq 0) {
- $uncoveredHandlers += [PSCustomObject]@{
- Module = ($assembly.name -replace "MeAjudaAi\.Modules\.", "" -replace "\.Application", "")
- Handler = ($class.name -replace "MeAjudaAi\..+\.", "")
- Lines = $class.coverablelines
- }
- }
- }
-}
-
-if ($uncoveredHandlers.Count -gt 0) {
- Write-Host " 🔴 $($uncoveredHandlers.Count) HANDLERS SEM COVERAGE:" -ForegroundColor Red
- Write-Host ""
- foreach ($handler in $uncoveredHandlers | Sort-Object -Property Lines -Descending | Select-Object -First 5) {
- Write-Host " • $($handler.Module): " -NoNewline -ForegroundColor Yellow
- Write-Host "$($handler.Handler) " -NoNewline -ForegroundColor White
- Write-Host "($($handler.Lines) linhas)" -ForegroundColor DarkGray
- }
- Write-Host ""
-}
-
-# Identificar repositories sem coverage
-$uncoveredRepos = @()
-foreach ($assembly in $summary.coverage.assemblies) {
- if ($assembly.name -notmatch "Infrastructure" -or $assembly.name -match "Tests") { continue }
-
- foreach ($class in $assembly.classesinassembly) {
- if ($class.name -match "Repository$" -and $class.coverage -eq 0) {
- $uncoveredRepos += [PSCustomObject]@{
- Module = ($assembly.name -replace "MeAjudaAi\.Modules\.", "" -replace "\.Infrastructure", "")
- Repository = ($class.name -replace "MeAjudaAi\..+\.", "")
- Lines = $class.coverablelines
- }
- }
- }
-}
-
-if ($uncoveredRepos.Count -gt 0) {
- Write-Host " 🔴 $($uncoveredRepos.Count) REPOSITORIES SEM COVERAGE:" -ForegroundColor Red
- Write-Host ""
- foreach ($repo in $uncoveredRepos | Sort-Object -Property Lines -Descending) {
- Write-Host " • $($repo.Module): " -NoNewline -ForegroundColor Yellow
- Write-Host "$($repo.Repository) " -NoNewline -ForegroundColor White
- Write-Host "($($repo.Lines) linhas)" -ForegroundColor DarkGray
- }
- Write-Host ""
-}
-
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-Write-Host "📖 Detalhes completos: " -NoNewline -ForegroundColor White
-Write-Host "CoverageReport\index.html" -ForegroundColor Cyan
-Write-Host "📋 Plano de ação: " -NoNewline -ForegroundColor White
-Write-Host "docs\testing\coverage-improvement-plan.md" -ForegroundColor Cyan
-Write-Host ""
diff --git a/scripts/deploy.sh b/scripts/deploy.sh
deleted file mode 100644
index 47cbc5d05..000000000
--- a/scripts/deploy.sh
+++ /dev/null
@@ -1,401 +0,0 @@
-#!/bin/bash
-
-# =============================================================================
-# MeAjudaAi Azure Infrastructure Deployment Script
-# =============================================================================
-# Script para deploy automatizado da infraestrutura Azure usando Bicep.
-# Suporta múltiplos ambientes (dev, prod) com configurações específicas.
-#
-# Uso:
-# ./scripts/deploy.sh [opções]
-#
-# Argumentos:
-# ambiente Ambiente de destino (dev, prod)
-# localização Região Azure (ex: brazilsouth, eastus)
-#
-# Opções:
-# -h, --help Mostra esta ajuda
-# -v, --verbose Modo verboso
-# -g, --resource-group Nome personalizado do resource group
-# -d, --dry-run Simula o deploy sem executar
-# -f, --force Força o deploy mesmo com warnings
-# --skip-validation Pula validação do template
-# --what-if Mostra o que seria alterado sem executar
-#
-# Exemplos:
-# ./scripts/deploy.sh dev brazilsouth # Deploy desenvolvimento
-# ./scripts/deploy.sh prod brazilsouth -v # Deploy produção verboso
-# ./scripts/deploy.sh prod eastus --what-if # Simular produção
-#
-# Dependências:
-# - Azure CLI autenticado
-# - Permissões Contributor no resource group
-# - Templates Bicep na pasta infrastructure/
-# =============================================================================
-
-set -e # Para em caso de erro
-
-# === Configurações ===
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
-INFRASTRUCTURE_DIR="$PROJECT_ROOT/infrastructure"
-BICEP_FILE="$INFRASTRUCTURE_DIR/main.bicep"
-
-# === Variáveis de Controle ===
-VERBOSE=false
-DRY_RUN=false
-FORCE=false
-SKIP_VALIDATION=false
-WHAT_IF=false
-CUSTOM_RESOURCE_GROUP=""
-
-# === Cores para output ===
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-# === Função de ajuda ===
-show_help() {
- sed -n '/^# =/,/^# =/p' "$0" | sed 's/^# //g' | sed 's/^=.*//g'
-}
-
-# === Funções de Logging ===
-print_header() {
- echo -e "${BLUE}===================================================================${NC}"
- echo -e "${BLUE} $1${NC}"
- echo -e "${BLUE}===================================================================${NC}"
-}
-
-print_info() {
- echo -e "${BLUE}[INFO]${NC} $1"
-}
-
-print_success() {
- echo -e "${GREEN}[SUCCESS]${NC} $1"
-}
-
-print_warning() {
- echo -e "${YELLOW}[WARNING]${NC} $1"
-}
-
-print_error() {
- echo -e "${RED}[ERROR]${NC} $1"
-}
-
-print_verbose() {
- if [ "$VERBOSE" = true ]; then
- echo -e "${CYAN}[VERBOSE]${NC} $1"
- fi
-}
-
-# === Parsing de argumentos ===
-ENVIRONMENT=""
-LOCATION=""
-
-while [[ $# -gt 0 ]]; do
- case $1 in
- -h|--help)
- show_help
- exit 0
- ;;
- -v|--verbose)
- VERBOSE=true
- shift
- ;;
- -g|--resource-group)
- CUSTOM_RESOURCE_GROUP="$2"
- shift 2
- ;;
- -d|--dry-run)
- DRY_RUN=true
- shift
- ;;
- -f|--force)
- FORCE=true
- shift
- ;;
- --skip-validation)
- SKIP_VALIDATION=true
- shift
- ;;
- --what-if)
- WHAT_IF=true
- shift
- ;;
- -*)
- print_error "Opção desconhecida: $1"
- show_help
- exit 1
- ;;
- *)
- if [ -z "$ENVIRONMENT" ]; then
- ENVIRONMENT="$1"
- elif [ -z "$LOCATION" ]; then
- LOCATION="$1"
- else
- print_error "Muitos argumentos fornecidos"
- show_help
- exit 1
- fi
- shift
- ;;
- esac
-done
-
-# === Validação de Argumentos ===
-if [ -z "$ENVIRONMENT" ] || [ -z "$LOCATION" ]; then
- print_error "Ambiente e localização são obrigatórios"
- show_help
- exit 1
-fi
-
-# === Configuração de Variáveis ===
-case $ENVIRONMENT in
- dev|prod)
- print_verbose "Ambiente válido: $ENVIRONMENT"
- ;;
- *)
- print_error "Ambiente inválido: $ENVIRONMENT. Deve ser: dev ou prod"
- exit 1
- ;;
-esac
-
-RESOURCE_GROUP=${CUSTOM_RESOURCE_GROUP:-"meajudaai-${ENVIRONMENT}"}
-DEPLOYMENT_NAME="deploy-$(date +%Y%m%d-%H%M%S)"
-
-print_verbose "Configurações:"
-print_verbose " Ambiente: $ENVIRONMENT"
-print_verbose " Localização: $LOCATION"
-print_verbose " Resource Group: $RESOURCE_GROUP"
-print_verbose " Deployment Name: $DEPLOYMENT_NAME"
-
-# === Navegar para raiz do projeto ===
-cd "$PROJECT_ROOT"
-
-# === Verificação de Pré-requisitos ===
-check_prerequisites() {
- print_header "Verificando Pré-requisitos"
-
- # Verificar Azure CLI
- if ! command -v az &> /dev/null; then
- print_error "Azure CLI não encontrado. Instale o Azure CLI."
- exit 1
- fi
-
- print_verbose "Azure CLI encontrado"
-
- # Verificar autenticação
- print_verbose "Verificando autenticação Azure..."
- if ! az account show &> /dev/null; then
- print_error "Não autenticado no Azure. Execute: az login"
- exit 1
- fi
-
- local subscription=$(az account show --query name -o tsv)
- print_info "Autenticado na subscription: $subscription"
-
- # Verificar arquivo Bicep
- if [ ! -f "$BICEP_FILE" ]; then
- print_error "Template Bicep não encontrado: $BICEP_FILE"
- exit 1
- fi
-
- print_verbose "Template Bicep encontrado: $BICEP_FILE"
-
- print_success "Todos os pré-requisitos verificados!"
-}
-
-# === Validação do Template ===
-validate_template() {
- if [ "$SKIP_VALIDATION" = true ]; then
- print_warning "Pulando validação do template..."
- return 0
- fi
-
- print_header "Validando Template Bicep"
-
- print_info "Executando validação do template..."
-
- local validation_output
- if validation_output=$(az deployment group validate \
- --resource-group "$RESOURCE_GROUP" \
- --template-file "$BICEP_FILE" \
- --parameters environmentName="$ENVIRONMENT" location="$LOCATION" \
- 2>&1); then
- print_success "Template válido!"
- if [ "$VERBOSE" = true ]; then
- echo "$validation_output"
- fi
- else
- print_error "Falha na validação do template:"
- echo "$validation_output"
-
- if [ "$FORCE" = false ]; then
- exit 1
- else
- print_warning "Continuando devido ao flag --force"
- fi
- fi
-}
-
-# === What-If Analysis ===
-run_what_if() {
- print_header "Análise What-If"
-
- print_info "Analisando mudanças que seriam aplicadas..."
-
- az deployment group what-if \
- --resource-group "$RESOURCE_GROUP" \
- --template-file "$BICEP_FILE" \
- --parameters environmentName="$ENVIRONMENT" location="$LOCATION"
-
- print_info "Análise what-if concluída."
-}
-
-# === Criação do Resource Group ===
-ensure_resource_group() {
- print_header "Verificando Resource Group"
-
- if az group show --name "$RESOURCE_GROUP" &> /dev/null; then
- print_info "Resource group '$RESOURCE_GROUP' já existe"
- else
- print_info "Criando resource group '$RESOURCE_GROUP'..."
-
- if [ "$DRY_RUN" = false ]; then
- az group create \
- --name "$RESOURCE_GROUP" \
- --location "$LOCATION" \
- --tags environment="$ENVIRONMENT" project="meajudaai"
-
- print_success "Resource group criado com sucesso!"
- else
- print_info "[DRY-RUN] Resource group seria criado"
- fi
- fi
-}
-
-# === Deploy da Infraestrutura ===
-deploy_infrastructure() {
- print_header "Deploying Infraestrutura Azure"
-
- if [ "$DRY_RUN" = true ]; then
- print_info "[DRY-RUN] Deploy seria executado com os seguintes parâmetros:"
- print_info " Resource Group: $RESOURCE_GROUP"
- print_info " Template: $BICEP_FILE"
- print_info " Environment: $ENVIRONMENT"
- print_info " Location: $LOCATION"
- return 0
- fi
-
- print_info "Iniciando deploy da infraestrutura..."
- print_info " Deployment Name: $DEPLOYMENT_NAME"
-
- local deploy_args=""
- if [ "$VERBOSE" = true ]; then
- deploy_args="--verbose"
- fi
-
- # Executar deploy
- az deployment group create \
- --name "$DEPLOYMENT_NAME" \
- --resource-group "$RESOURCE_GROUP" \
- --template-file "$BICEP_FILE" \
- --parameters environmentName="$ENVIRONMENT" location="$LOCATION" \
- $deploy_args
-
- if [ $? -eq 0 ]; then
- print_success "Deploy concluído com sucesso!"
- else
- print_error "Falha no deploy"
- exit 1
- fi
-}
-
-# === Obter Outputs do Deploy ===
-get_deployment_outputs() {
- print_header "Obtendo Outputs do Deploy"
-
- if [ "$DRY_RUN" = true ]; then
- print_info "[DRY-RUN] Outputs seriam obtidos"
- return 0
- fi
-
- print_info "Obtendo outputs do deployment..."
-
- # Listar todos os outputs
- local outputs=$(az deployment group show \
- --name "$DEPLOYMENT_NAME" \
- --resource-group "$RESOURCE_GROUP" \
- --query "properties.outputs" \
- --output table)
-
- if [ -n "$outputs" ]; then
- print_success "Outputs do deployment:"
- echo "$outputs"
-
- # Salvar outputs em arquivo
- local outputs_file="$PROJECT_ROOT/deployment-outputs-$ENVIRONMENT.json"
- az deployment group show \
- --name "$DEPLOYMENT_NAME" \
- --resource-group "$RESOURCE_GROUP" \
- --query "properties.outputs" \
- --output json > "$outputs_file"
-
- print_info "Outputs salvos em: $outputs_file"
- else
- print_warning "Nenhum output encontrado no deployment"
- fi
-}
-
-# === Relatório Final ===
-show_summary() {
- print_header "Resumo do Deploy"
-
- print_info "Deployment executado com sucesso!"
- print_info " Ambiente: $ENVIRONMENT"
- print_info " Resource Group: $RESOURCE_GROUP"
- print_info " Localização: $LOCATION"
-
- if [ "$DRY_RUN" = false ]; then
- print_info " Deployment Name: $DEPLOYMENT_NAME"
-
- # Link para o portal
- local portal_url="https://portal.azure.com/#@/resource/subscriptions/$(az account show --query id -o tsv)/resourceGroups/$RESOURCE_GROUP"
- print_info " Portal Azure: $portal_url"
- fi
-
- print_success "Deploy concluído! 🚀"
-}
-
-# === Execução Principal ===
-main() {
- local start_time=$(date +%s)
-
- # Banner
- print_header "MeAjudaAi Infrastructure Deployment"
-
- check_prerequisites
- ensure_resource_group
- validate_template
-
- if [ "$WHAT_IF" = true ]; then
- run_what_if
- print_info "Análise what-if concluída. Use sem --what-if para executar o deploy."
- exit 0
- fi
-
- deploy_infrastructure
- get_deployment_outputs
-
- local end_time=$(date +%s)
- local duration=$((end_time - start_time))
-
- show_summary
- print_info "Tempo total: ${duration}s"
-}
-
-# === Execução ===
-main "$@"
\ No newline at end of file
diff --git a/scripts/dev.sh b/scripts/dev.sh
deleted file mode 100644
index d3a8d118b..000000000
--- a/scripts/dev.sh
+++ /dev/null
@@ -1,412 +0,0 @@
-#!/bin/bash
-
-# =============================================================================
-# MeAjudaAi Development Script - Ambiente de Desenvolvimento Local
-# =============================================================================
-# Script consolidado para desenvolvimento local da aplicação MeAjudaAi.
-# Inclui configuração de infraestrutura, testes e execução local.
-#
-# Uso:
-# ./scripts/dev.sh [opções]
-#
-# Opções:
-# -h, --help Mostra esta ajuda
-# -v, --verbose Modo verboso
-# -s, --simple Execução simples sem Azure
-# -t, --test-only Apenas executa testes
-# -b, --build-only Apenas compila
-# --skip-deps Pula verificação de dependências
-# --skip-tests Pula execução de testes
-#
-# Exemplos:
-# ./scripts/dev.sh # Menu interativo
-# ./scripts/dev.sh --simple # Execução local simples
-# ./scripts/dev.sh --test-only # Apenas testes
-# ./scripts/dev.sh --build-only # Apenas build
-#
-# Dependências:
-# - .NET 8 SDK
-# - Docker Desktop
-# - Azure CLI (para modo completo)
-# - PowerShell (Windows)
-# =============================================================================
-
-set -e # Para em caso de erro
-
-# === Configurações ===
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
-RESOURCE_GROUP="meajudaai-dev"
-ENVIRONMENT_NAME="dev"
-BICEP_FILE="infrastructure/main.bicep"
-LOCATION="brazilsouth"
-PROJECT_DIR="src/Bootstrapper/MeAjudaAi.ApiService"
-APPHOST_DIR="src/Aspire/MeAjudaAi.AppHost"
-
-# === Variáveis de Controle ===
-VERBOSE=false
-SIMPLE_MODE=false
-TEST_ONLY=false
-BUILD_ONLY=false
-SKIP_DEPS=false
-SKIP_TESTS=false
-
-# === Cores para output ===
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-# === Função de ajuda ===
-show_help() {
- sed -n '/^# =/,/^# =/p' "$0" | sed 's/^# //g' | sed 's/^=.*//g'
-}
-
-# === Funções de Logging ===
-print_header() {
- echo -e "${BLUE}===================================================================${NC}"
- echo -e "${BLUE} $1${NC}"
- echo -e "${BLUE}===================================================================${NC}"
-}
-
-print_info() {
- echo -e "${GREEN}✅ $1${NC}"
-}
-
-print_warning() {
- echo -e "${YELLOW}⚠️ $1${NC}"
-}
-
-print_error() {
- echo -e "${RED}❌ $1${NC}"
-}
-
-print_verbose() {
- if [ "$VERBOSE" = true ]; then
- echo -e "${CYAN}🔍 $1${NC}"
- fi
-}
-
-# === Parsing de argumentos ===
-while [[ $# -gt 0 ]]; do
- case $1 in
- -h|--help)
- show_help
- exit 0
- ;;
- -v|--verbose)
- VERBOSE=true
- shift
- ;;
- -s|--simple)
- SIMPLE_MODE=true
- shift
- ;;
- -t|--test-only)
- TEST_ONLY=true
- shift
- ;;
- -b|--build-only)
- BUILD_ONLY=true
- shift
- ;;
- --skip-deps)
- SKIP_DEPS=true
- shift
- ;;
- --skip-tests)
- SKIP_TESTS=true
- shift
- ;;
- *)
- echo "Opção desconhecida: $1"
- show_help
- exit 1
- ;;
- esac
-done
-
-# === Navegar para raiz do projeto ===
-cd "$PROJECT_ROOT"
-
-# === Verificação de Dependências ===
-check_dependencies() {
- if [ "$SKIP_DEPS" = true ]; then
- print_info "Pulando verificação de dependências..."
- return 0
- fi
-
- print_header "Verificando Dependências"
-
- # Verificar .NET
- print_verbose "Verificando .NET SDK..."
- if ! command -v dotnet &> /dev/null; then
- print_error ".NET SDK não encontrado. Instale o .NET 8 SDK."
- exit 1
- fi
-
- # Verificar versão do .NET
- DOTNET_VERSION=$(dotnet --version)
- print_info ".NET SDK encontrado: $DOTNET_VERSION"
-
- # Verificar Docker
- print_verbose "Verificando Docker..."
- if ! command -v docker &> /dev/null; then
- print_error "Docker não encontrado. Instale o Docker Desktop."
- exit 1
- fi
-
- # Verificar se Docker está rodando
- if ! docker info &> /dev/null; then
- print_error "Docker não está rodando. Inicie o Docker Desktop."
- exit 1
- fi
-
- print_info "Docker encontrado e rodando."
-
- # Verificar Azure CLI (apenas se não for modo simples)
- if [ "$SIMPLE_MODE" = false ]; then
- print_verbose "Verificando Azure CLI..."
- if ! command -v az &> /dev/null; then
- print_warning "Azure CLI não encontrado. Modo simples será usado."
- SIMPLE_MODE=true
- else
- print_info "Azure CLI encontrado."
- fi
- fi
-
- print_info "Todas as dependências verificadas!"
-}
-
-# === Build da Solução ===
-build_solution() {
- print_header "Compilando Solução"
-
- print_info "Restaurando dependências..."
- dotnet restore
-
- print_info "Compilando solução..."
- if [ "$VERBOSE" = true ]; then
- dotnet build --no-restore --verbosity normal
- else
- dotnet build --no-restore --verbosity minimal
- fi
-
- if [ $? -eq 0 ]; then
- print_info "Build concluído com sucesso!"
- else
- print_error "Falha no build. Verifique os erros acima."
- exit 1
- fi
-}
-
-# === Execução de Testes ===
-run_tests() {
- if [ "$SKIP_TESTS" = true ]; then
- print_info "Pulando execução de testes..."
- return 0
- fi
-
- print_header "Executando Testes"
-
- print_info "Executando testes unitários..."
- if [ "$VERBOSE" = true ]; then
- dotnet test --no-build --verbosity normal
- else
- dotnet test --no-build --verbosity minimal
- fi
-
- if [ $? -eq 0 ]; then
- print_info "Todos os testes passaram!"
- else
- print_error "Alguns testes falharam. Verifique os resultados acima."
- exit 1
- fi
-}
-
-# === Azure Infrastructure Setup ===
-setup_azure_infrastructure() {
- print_header "Configurando Infraestrutura Azure"
-
- print_info "Fazendo login no Azure (se necessário)..."
- az account show > /dev/null 2>&1 || az login
-
- print_info "Deploying Bicep template..."
- DEPLOYMENT_NAME="sb-deployment-$(date +%s)"
-
- # Deploy the Bicep template first
- az deployment group create \
- --name "$DEPLOYMENT_NAME" \
- --resource-group "$RESOURCE_GROUP" \
- --template-file "$BICEP_FILE" \
- --parameters environmentName="$ENVIRONMENT_NAME" location="$LOCATION"
-
- # Get the Service Bus namespace and policy names from outputs
- NAMESPACE_NAME=$(az deployment group show \
- --name "$DEPLOYMENT_NAME" \
- --resource-group "$RESOURCE_GROUP" \
- --query "properties.outputs.serviceBusNamespace.value" \
- --output tsv)
-
- MANAGEMENT_POLICY_NAME=$(az deployment group show \
- --name "$DEPLOYMENT_NAME" \
- --resource-group "$RESOURCE_GROUP" \
- --query "properties.outputs.managementPolicyName.value" \
- --output tsv)
-
- # Get the connection string securely using Azure CLI
- OUTPUT_JSON=$(az servicebus namespace authorization-rule keys list \
- --resource-group "$RESOURCE_GROUP" \
- --namespace-name "$NAMESPACE_NAME" \
- --name "$MANAGEMENT_POLICY_NAME" \
- --query "primaryConnectionString" \
- --output tsv)
-
- if [ -z "$OUTPUT_JSON" ]; then
- print_error "Não foi possível extrair a ConnectionString do output do Bicep."
- exit 1
- fi
-
- print_info "ConnectionString obtida com sucesso."
- export Messaging__ServiceBus__ConnectionString="$OUTPUT_JSON"
-}
-
-# === Execução Local ===
-run_local() {
- print_header "Executando Aplicação Local"
-
- if [ "$SIMPLE_MODE" = false ]; then
- setup_azure_infrastructure
- else
- print_info "Modo simples - usando configurações locais..."
- export ASPNETCORE_ENVIRONMENT=Development
- fi
-
- print_info "Iniciando aplicação Aspire..."
- cd "$APPHOST_DIR"
- dotnet run
-}
-
-# === Configurar User Secrets ===
-setup_user_secrets() {
- print_header "Configurando User Secrets"
-
- print_info "Configurando secrets para o projeto API..."
- cd "$PROJECT_DIR"
-
- # Lista os secrets atuais
- print_info "Secrets atuais:"
- dotnet user-secrets list || print_warning "Nenhum secret configurado."
-
- echo ""
- print_info "Para adicionar um novo secret, use:"
- echo " dotnet user-secrets set \"chave\" \"valor\""
- echo ""
- print_info "Exemplo:"
- echo " dotnet user-secrets set \"ConnectionStrings:DefaultConnection\" \"sua-connection-string\""
-}
-
-# === Menu Principal ===
-show_menu() {
- print_header "MeAjudaAi Development Script"
- echo "Escolha uma opção:"
- echo ""
- echo " 1) ✅ Verificar dependências"
- echo " 2) 🔨 Build completo (compile + teste)"
- echo " 3) 🔧 Apenas compilar"
- echo " 4) 🧪 Apenas testar"
- echo " 5) 🚀 Executar local (com Azure)"
- echo " 6) ⚡ Executar local (simples - sem Azure)"
- echo " 7) 🔐 Configurar user-secrets"
- echo " 8) 🎯 Setup completo (build + test + run)"
- echo " 0) ❌ Sair"
- echo ""
- read -p "Digite sua escolha [0-8]: " choice
-}
-
-# === Processamento de Menu ===
-process_menu_choice() {
- case $choice in
- 1)
- check_dependencies
- ;;
- 2)
- check_dependencies
- build_solution
- run_tests
- ;;
- 3)
- check_dependencies
- build_solution
- ;;
- 4)
- run_tests
- ;;
- 5)
- check_dependencies
- build_solution
- run_tests
- run_local
- ;;
- 6)
- SIMPLE_MODE=true
- check_dependencies
- build_solution
- run_tests
- run_local
- ;;
- 7)
- setup_user_secrets
- ;;
- 8)
- check_dependencies
- build_solution
- run_tests
- run_local
- ;;
- 0)
- print_info "Saindo..."
- exit 0
- ;;
- *)
- print_error "Opção inválida!"
- ;;
- esac
-}
-
-# === Execução Principal ===
-main() {
- # Execução baseada em argumentos
- if [ "$TEST_ONLY" = true ]; then
- run_tests
- exit 0
- fi
-
- if [ "$BUILD_ONLY" = true ]; then
- check_dependencies
- build_solution
- exit 0
- fi
-
- # Se há argumentos específicos, executa diretamente
- if [ "$SIMPLE_MODE" = true ] && [ $# -gt 0 ]; then
- check_dependencies
- build_solution
- run_tests
- run_local
- exit 0
- fi
-
- # Menu interativo
- while true; do
- show_menu
- process_menu_choice
- echo ""
- read -p "Pressione Enter para continuar..."
- done
-}
-
-# === Execução ===
-main "$@"
\ No newline at end of file
diff --git a/scripts/find-coverage-gaps.ps1 b/scripts/find-coverage-gaps.ps1
deleted file mode 100644
index 85c9f2898..000000000
--- a/scripts/find-coverage-gaps.ps1
+++ /dev/null
@@ -1,266 +0,0 @@
-#!/usr/bin/env pwsh
-<#
-.SYNOPSIS
- Identifica gaps de code coverage no projeto MeAjudaAi
-
-.DESCRIPTION
- Analisa o código fonte e identifica:
- - CommandHandlers sem testes
- - QueryHandlers sem testes
- - Validators sem testes
- - Value Objects sem testes
- - Repositories sem testes
-
-.EXAMPLE
- .\scripts\find-coverage-gaps.ps1
-#>
-
-param(
- [switch]$Verbose
-)
-
-$ErrorActionPreference = "Stop"
-
-Write-Host "🔍 Analisando gaps de code coverage..." -ForegroundColor Cyan
-Write-Host ""
-
-# ============================================================================
-# 1. Command/Query Handlers
-# ============================================================================
-
-Write-Host "📋 COMMAND/QUERY HANDLERS SEM TESTES" -ForegroundColor Yellow
-Write-Host "=" * 80
-
-$handlers = Get-ChildItem -Path "src/Modules/*/Application" -Recurse -Filter "*Handler.cs" |
- Where-Object { $_.Name -match "(Command|Query)Handler\.cs$" }
-
-$missingHandlerTests = @()
-
-foreach ($handler in $handlers) {
- $handlerName = $handler.BaseName
- $testName = "${handlerName}Tests"
- $module = ($handler.FullName -split "Modules\\")[1] -split "\\" | Select-Object -First 1
-
- # Procurar teste correspondente
- $testPath = "src/Modules/$module/Tests/**/${testName}.cs"
- $testExists = Test-Path $testPath -PathType Leaf
-
- if (-not $testExists) {
- # Tentar buscar em qualquer lugar dentro de Tests
- $searchResult = Get-ChildItem -Path "src/Modules/$module/Tests" -Recurse -Filter "${testName}.cs" -ErrorAction SilentlyContinue
-
- if (-not $searchResult) {
- $missingHandlerTests += [PSCustomObject]@{
- Module = $module
- Handler = $handlerName
- ExpectedTest = $testName
- Type = if ($handlerName -match "Command") { "Command" } else { "Query" }
- }
- }
- }
-}
-
-if ($missingHandlerTests.Count -eq 0) {
- Write-Host "✅ Todos os handlers possuem testes!" -ForegroundColor Green
-} else {
- $missingHandlerTests | Format-Table -AutoSize
- Write-Host "❌ Total: $($missingHandlerTests.Count) handlers sem testes" -ForegroundColor Red
-}
-
-Write-Host ""
-
-# ============================================================================
-# 2. Validators
-# ============================================================================
-
-Write-Host "✅ VALIDATORS (FLUENTVALIDATION) SEM TESTES" -ForegroundColor Yellow
-Write-Host "=" * 80
-
-$validators = Get-ChildItem -Path "src/Modules/*/Application" -Recurse -Filter "*Validator.cs" |
- Where-Object { $_.Name -match "Validator\.cs$" -and $_.Name -notmatch "Tests" }
-
-$missingValidatorTests = @()
-
-foreach ($validator in $validators) {
- $validatorName = $validator.BaseName
- $testName = "${validatorName}Tests"
- $module = ($validator.FullName -split "Modules\\")[1] -split "\\" | Select-Object -First 1
-
- $searchResult = Get-ChildItem -Path "src/Modules/$module/Tests" -Recurse -Filter "${testName}.cs" -ErrorAction SilentlyContinue
-
- if (-not $searchResult) {
- $missingValidatorTests += [PSCustomObject]@{
- Module = $module
- Validator = $validatorName
- ExpectedTest = $testName
- }
- }
-}
-
-if ($missingValidatorTests.Count -eq 0) {
- Write-Host "✅ Todos os validators possuem testes!" -ForegroundColor Green
-} else {
- $missingValidatorTests | Format-Table -AutoSize
- Write-Host "❌ Total: $($missingValidatorTests.Count) validators sem testes" -ForegroundColor Red
-}
-
-Write-Host ""
-
-# ============================================================================
-# 3. Value Objects (Domain)
-# ============================================================================
-
-Write-Host "💎 VALUE OBJECTS (DOMAIN) SEM TESTES" -ForegroundColor Yellow
-Write-Host "=" * 80
-
-$commonValueObjects = @(
- "Address", "Email", "PhoneNumber", "CPF", "CNPJ",
- "DocumentType", "Money", "DateRange", "TimeSlot"
-)
-
-$missingVOTests = @()
-
-foreach ($module in (Get-ChildItem -Path "src/Modules" -Directory).Name) {
- $domainPath = "src/Modules/$module/Domain"
-
- if (Test-Path $domainPath) {
- # Buscar por Value Objects comuns
- foreach ($vo in $commonValueObjects) {
- $voFile = Get-ChildItem -Path $domainPath -Recurse -Filter "${vo}.cs" -ErrorAction SilentlyContinue
-
- if ($voFile) {
- $testName = "${vo}Tests"
- $testExists = Get-ChildItem -Path "src/Modules/$module/Tests" -Recurse -Filter "${testName}.cs" -ErrorAction SilentlyContinue
-
- if (-not $testExists) {
- $missingVOTests += [PSCustomObject]@{
- Module = $module
- ValueObject = $vo
- ExpectedTest = $testName
- }
- }
- }
- }
- }
-}
-
-if ($missingVOTests.Count -eq 0) {
- Write-Host "✅ Principais Value Objects possuem testes!" -ForegroundColor Green
-} else {
- $missingVOTests | Format-Table -AutoSize
- Write-Host "❌ Total: $($missingVOTests.Count) value objects sem testes" -ForegroundColor Red
-}
-
-Write-Host ""
-
-# ============================================================================
-# 4. Repositories
-# ============================================================================
-
-Write-Host "🗄️ REPOSITORIES SEM TESTES" -ForegroundColor Yellow
-Write-Host "=" * 80
-
-$repositories = Get-ChildItem -Path "src/Modules/*/Infrastructure" -Recurse -Filter "*Repository.cs" |
- Where-Object { $_.Name -match "Repository\.cs$" -and $_.Name -notmatch "Interface|Tests" }
-
-$missingRepoTests = @()
-
-foreach ($repo in $repositories) {
- $repoName = $repo.BaseName
- $testName = "${repoName}Tests"
- $module = ($repo.FullName -split "Modules\\")[1] -split "\\" | Select-Object -First 1
-
- $searchResult = Get-ChildItem -Path "src/Modules/$module/Tests" -Recurse -Filter "${testName}.cs" -ErrorAction SilentlyContinue
-
- if (-not $searchResult) {
- $missingRepoTests += [PSCustomObject]@{
- Module = $module
- Repository = $repoName
- ExpectedTest = $testName
- }
- }
-}
-
-if ($missingRepoTests.Count -eq 0) {
- Write-Host "✅ Todos os repositories possuem testes!" -ForegroundColor Green
-} else {
- $missingRepoTests | Format-Table -AutoSize
- Write-Host "❌ Total: $($missingRepoTests.Count) repositories sem testes" -ForegroundColor Red
-}
-
-Write-Host ""
-
-# ============================================================================
-# 5. Resumo
-# ============================================================================
-
-Write-Host "📊 RESUMO DE GAPS" -ForegroundColor Cyan
-Write-Host "=" * 80
-
-$totalGaps = $missingHandlerTests.Count + $missingValidatorTests.Count +
- $missingVOTests.Count + $missingRepoTests.Count
-
-Write-Host "Handlers sem testes: $($missingHandlerTests.Count)" -ForegroundColor $(if ($missingHandlerTests.Count -eq 0) { "Green" } else { "Red" })
-Write-Host "Validators sem testes: $($missingValidatorTests.Count)" -ForegroundColor $(if ($missingValidatorTests.Count -eq 0) { "Green" } else { "Red" })
-Write-Host "Value Objects sem testes: $($missingVOTests.Count)" -ForegroundColor $(if ($missingVOTests.Count -eq 0) { "Green" } else { "Red" })
-Write-Host "Repositories sem testes: $($missingRepoTests.Count)" -ForegroundColor $(if ($missingRepoTests.Count -eq 0) { "Green" } else { "Red" })
-Write-Host ""
-Write-Host "TOTAL DE GAPS: $totalGaps" -ForegroundColor $(if ($totalGaps -eq 0) { "Green" } else { "Red" })
-
-Write-Host ""
-
-# ============================================================================
-# 6. Estimativa de Impacto no Coverage
-# ============================================================================
-
-Write-Host "📈 ESTIMATIVA DE IMPACTO NO COVERAGE" -ForegroundColor Cyan
-Write-Host "=" * 80
-
-# Estimativas conservadoras:
-# - Cada handler: +0.5pp
-# - Cada validator: +0.3pp
-# - Cada Value Object: +0.4pp
-# - Cada repository: +0.6pp
-
-$estimatedImpact = ($missingHandlerTests.Count * 0.5) +
- ($missingValidatorTests.Count * 0.3) +
- ($missingVOTests.Count * 0.4) +
- ($missingRepoTests.Count * 0.6)
-
-Write-Host "Coverage atual (pipeline): 35.11%"
-Write-Host "Coverage estimado após fixes: $(35.11 + $estimatedImpact)% (+$($estimatedImpact)pp)"
-Write-Host ""
-
-if ($estimatedImpact -ge 20) {
- Write-Host "✅ Potencial para atingir meta de 55%!" -ForegroundColor Green
-} elseif ($estimatedImpact -ge 10) {
- Write-Host "⚠️ Bom progresso, mas pode precisar de mais testes" -ForegroundColor Yellow
-} else {
- Write-Host "⚠️ Impacto baixo, considere outras áreas" -ForegroundColor Yellow
-}
-
-Write-Host ""
-Write-Host "🎯 PRÓXIMOS PASSOS" -ForegroundColor Cyan
-Write-Host "=" * 80
-Write-Host "1. Priorize handlers críticos (Commands > Queries)"
-Write-Host "2. Adicione testes para validators (rápido, alto impacto)"
-Write-Host "3. Teste Value Objects com casos edge (validações)"
-Write-Host "4. Repositories: use InMemory DbContext ou mocks"
-Write-Host ""
-
-# Exportar para arquivo CSV (opcional)
-if ($Verbose) {
- $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
- $reportPath = "coverage-gaps-$timestamp.csv"
-
- $allGaps = @()
- $allGaps += $missingHandlerTests | Select-Object @{N='Category';E={'Handler'}}, Module, @{N='Name';E={$_.Handler}}, ExpectedTest
- $allGaps += $missingValidatorTests | Select-Object @{N='Category';E={'Validator'}}, Module, @{N='Name';E={$_.Validator}}, ExpectedTest
- $allGaps += $missingVOTests | Select-Object @{N='Category';E={'ValueObject'}}, Module, @{N='Name';E={$_.ValueObject}}, ExpectedTest
- $allGaps += $missingRepoTests | Select-Object @{N='Category';E={'Repository'}}, Module, @{N='Name';E={$_.Repository}}, ExpectedTest
-
- $allGaps | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
- Write-Host "📄 Relatório exportado: $reportPath" -ForegroundColor Green
-}
-
-exit $totalGaps
diff --git a/scripts/generate-clean-coverage.ps1 b/scripts/generate-clean-coverage.ps1
deleted file mode 100644
index 748e86c6e..000000000
--- a/scripts/generate-clean-coverage.ps1
+++ /dev/null
@@ -1,59 +0,0 @@
-# Script para gerar coverage EXCLUINDO código gerado do compilador
-# Uso: .\scripts\generate-clean-coverage.ps1
-
-Write-Host "🔬 Gerando Code Coverage (EXCLUINDO código gerado)" -ForegroundColor Cyan
-Write-Host ""
-
-# Limpar coverage anterior
-Write-Host "1. Limpando diretório coverage..." -ForegroundColor Yellow
-if (Test-Path coverage) {
- Remove-Item coverage -Recurse -Force
-}
-
-# Rodar testes com exclusões configuradas
-Write-Host "2. Rodando testes com Coverlet (pode demorar ~25 minutos)..." -ForegroundColor Yellow
-Write-Host " Excluindo padrões: **/*OpenApi*.generated.cs, **/*System.Runtime.CompilerServices*.cs, **/*RegexGenerator.g.cs" -ForegroundColor Gray
-Write-Host ""
-
-dotnet test `
- --configuration Debug `
- --collect:"XPlat Code Coverage" `
- --results-directory:"coverage" `
- -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.ExcludeByFile="**/*OpenApi*.generated.cs,**/*System.Runtime.CompilerServices*.cs,**/*RegexGenerator.g.cs"
-
-if ($LASTEXITCODE -ne 0) {
- Write-Host "❌ Erro ao rodar testes!" -ForegroundColor Red
- exit 1
-}
-
-Write-Host ""
-Write-Host "✅ Testes concluídos com sucesso!" -ForegroundColor Green
-Write-Host ""
-
-# Gerar relatório consolidado
-Write-Host "3. Gerando relatório HTML consolidado..." -ForegroundColor Yellow
-
-reportgenerator `
- -reports:"coverage/**/coverage.cobertura.xml" `
- -targetdir:"coverage/report" `
- -reporttypes:"Html;TextSummary;JsonSummary" `
- -assemblyfilters:"+*;-*.Tests;-*.Tests.*;-*Test*;-testhost;-MeAjudaAi.AppHost;-MeAjudaAi.ServiceDefaults" `
- -classfilters:"-*Migrations*;-*Migration;-*MigrationBuilder*;-*DbContextModelSnapshot*;-*OpenApi.Generated*;-*.Keycloak.*;-*.Monitoring.*;-*NoOp*;-*RabbitMq*;-*ServiceBus*;-*Hangfire*;-*.Jobs.*;-*Options;-*BaseDesignTimeDbContextFactory*;-*SchemaPermissionsManager;-*SimpleHostEnvironment;-*CacheWarmupService;-*GeoPointConverter;-*ModuleNames;-*ModuleApiInfo;-*MessagingExtensions;-*ICacheableQuery"
-
-Write-Host ""
-Write-Host "✅ Relatório gerado em coverage/report/index.html" -ForegroundColor Green
-Write-Host ""
-
-# Exibir sumário
-Write-Host "📊 Resumo de Coverage (SEM código gerado):" -ForegroundColor Cyan
-Get-Content coverage/report/Summary.txt | Select-Object -First 20
-
-Write-Host ""
-Write-Host "🌐 Abrindo relatório no navegador..." -ForegroundColor Yellow
-Start-Process (Resolve-Path coverage/report/index.html).Path
-
-Write-Host ""
-Write-Host "✅ CONCLUÍDO!" -ForegroundColor Green
-Write-Host ""
-Write-Host "O relatório agora exclui código gerado pelo compilador (OpenApi, CompilerServices, RegexGenerator)." -ForegroundColor Yellow
-Write-Host "Compare com relatórios anteriores para ver a cobertura real do código manual." -ForegroundColor Green
diff --git a/scripts/monitor-coverage.ps1 b/scripts/monitor-coverage.ps1
deleted file mode 100644
index a63ffb08d..000000000
--- a/scripts/monitor-coverage.ps1
+++ /dev/null
@@ -1,112 +0,0 @@
-# Monitor de Coverage - Processos Paralelos
-# Uso: .\scripts\monitor-coverage.ps1
-
-Write-Host "📊 MONITORANDO COVERAGE - LOCAL E PIPELINE" -ForegroundColor Cyan
-Write-Host "═══════════════════════════════════════════════════════" -ForegroundColor Gray
-Write-Host ""
-
-# Verificar job local
-$job = Get-Job -Name "CleanCoverage" -ErrorAction SilentlyContinue
-
-if ($job) {
- Write-Host "🖥️ COVERAGE LOCAL (Background Job):" -ForegroundColor Yellow
- Write-Host "───────────────────────────────────" -ForegroundColor Gray
- Write-Host " Estado: $($job.State)" -ForegroundColor $(if ($job.State -eq 'Running') { 'Cyan' } elseif ($job.State -eq 'Completed') { 'Green' } else { 'Red' })
- Write-Host " Job ID: $($job.Id)"
- Write-Host ""
-
- if ($job.State -eq 'Running') {
- Write-Host " ⏳ Ainda em execução..." -ForegroundColor Cyan
- Write-Host " 💡 Para ver progresso: Receive-Job -Id $($job.Id) -Keep" -ForegroundColor Gray
- }
- elseif ($job.State -eq 'Completed') {
- Write-Host " ✅ CONCLUÍDO!" -ForegroundColor Green
- Write-Host ""
- Write-Host " 📄 Últimas 30 linhas do output:" -ForegroundColor White
- Write-Host " ───────────────────────────────────" -ForegroundColor Gray
- Receive-Job -Id $job.Id -Keep | Select-Object -Last 30
-
- # Verificar se relatório foi gerado
- $summaryPath = Join-Path $PSScriptRoot "..\coverage\report\Summary.txt"
- if (Test-Path $summaryPath) {
- Write-Host ""
- Write-Host " 📊 RESUMO DE COVERAGE:" -ForegroundColor Green
- Write-Host " ───────────────────────────────────" -ForegroundColor Gray
- try {
- Get-Content $summaryPath -ErrorAction Stop | Select-Object -First 15
- } catch {
- Write-Host " ⚠️ Erro ao ler arquivo de resumo: $_" -ForegroundColor Yellow
- }
- }
- }
- elseif ($job.State -eq 'Failed') {
- Write-Host " ❌ ERRO!" -ForegroundColor Red
- Receive-Job -Id $job.Id -Keep
- }
-}
-else {
- Write-Host "🖥️ COVERAGE LOCAL: Não encontrado" -ForegroundColor Red
- Write-Host " 💡 Execute: .\scripts\generate-clean-coverage.ps1" -ForegroundColor Gray
-}
-
-Write-Host ""
-Write-Host "═══════════════════════════════════════════════════════" -ForegroundColor Gray
-Write-Host ""
-
-# Link para pipeline
-try {
- $branch = git rev-parse --abbrev-ref HEAD 2>$null
- if ($LASTEXITCODE -ne 0) { throw }
-} catch {
- $branch = "unknown-branch"
- Write-Warning "Git não disponível ou não está em um repositório - usando branch padrão"
-}
-
-try {
- $commit = git rev-parse --short HEAD 2>$null
- if ($LASTEXITCODE -ne 0) { throw }
-} catch {
- $commit = "unknown-commit"
- Write-Warning "Não foi possível obter commit hash"
-}
-
-try {
- $commitMsg = git log -1 --pretty=%s 2>$null
- if ($LASTEXITCODE -ne 0) { throw }
-} catch {
- $commitMsg = "unknown-message"
- Write-Warning "Não foi possível obter mensagem do commit"
-}
-
-try {
- $repoUrl = (git remote get-url origin 2>$null) -replace '\.git$', '' -replace '^git@github\.com:', 'https://github.com/'
- if ($LASTEXITCODE -ne 0 -or -not $repoUrl) { throw }
-} catch {
- $repoUrl = "https://github.com/frigini/MeAjudaAi"
- Write-Warning "Não foi possível obter URL do repositório - usando padrão"
-}
-
-Write-Host "🌐 PIPELINE GITHUB:" -ForegroundColor Yellow
-Write-Host "───────────────────────────────────" -ForegroundColor Gray
-Write-Host " $repoUrl/actions" -ForegroundColor Cyan
-Write-Host ""
-Write-Host " Branch: $branch" -ForegroundColor White
-Write-Host " Commit: $commit ($commitMsg)" -ForegroundColor White
-Write-Host ""
-
-Write-Host "═══════════════════════════════════════════════════════" -ForegroundColor Gray
-Write-Host ""
-Write-Host "🔄 COMANDOS ÚTEIS:" -ForegroundColor Magenta
-Write-Host ""
-Write-Host " Ver progresso local:" -ForegroundColor White
-Write-Host " Receive-Job -Name CleanCoverage -Keep" -ForegroundColor Gray
-Write-Host ""
-Write-Host " Remover job concluído:" -ForegroundColor White
-Write-Host " Remove-Job -Name CleanCoverage" -ForegroundColor Gray
-Write-Host ""
-Write-Host " Abrir relatório local:" -ForegroundColor White
-Write-Host " Start-Process (Join-Path \$PSScriptRoot \"..\\coverage\\report\\index.html\")" -ForegroundColor Gray
-Write-Host ""
-Write-Host " Re-executar este monitor:" -ForegroundColor White
-Write-Host " .\scripts\monitor-coverage.ps1" -ForegroundColor Gray
-Write-Host ""
diff --git a/scripts/optimize.sh b/scripts/optimize.sh
deleted file mode 100644
index a1ed77963..000000000
--- a/scripts/optimize.sh
+++ /dev/null
@@ -1,405 +0,0 @@
-#!/bin/bash
-
-# =============================================================================
-# MeAjudaAi Test Performance Optimization Script
-# =============================================================================
-# Script para aplicar otimizações de performance durante execução de testes.
-# Configura variáveis de ambiente e otimizações específicas para melhorar
-# a velocidade de execução dos testes em até 70%.
-#
-# Uso:
-# ./scripts/optimize.sh [opções]
-# source ./scripts/optimize.sh # Para manter variáveis no shell atual
-#
-# Opções:
-# -h, --help Mostra esta ajuda
-# -v, --verbose Modo verboso
-# -r, --reset Remove otimizações (restaura padrões)
-# -t, --test Aplica e executa teste de performance
-# --docker-only Apenas otimizações Docker
-# --dotnet-only Apenas otimizações .NET
-#
-# Exemplos:
-# ./scripts/optimize.sh # Aplica todas as otimizações
-# ./scripts/optimize.sh --test # Aplica e testa performance
-# source ./scripts/optimize.sh # Mantém variáveis no shell
-#
-# Otimizações aplicadas:
-# - Docker/TestContainers (70% mais rápido)
-# - .NET Runtime (40% menos overhead)
-# - PostgreSQL (60% mais rápido setup)
-# - Logging reduzido (30% menos I/O)
-# =============================================================================
-
-# === Verificar se está sendo "sourced" ===
-SOURCED=false
-if [ "${BASH_SOURCE[0]}" != "${0}" ]; then
- SOURCED=true
-fi
-
-# === Configurações ===
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
-
-# === Variáveis de Controle ===
-VERBOSE=false
-RESET=false
-TEST_PERFORMANCE=false
-DOCKER_ONLY=false
-DOTNET_ONLY=false
-
-# === Cores para output ===
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-# === Função de ajuda ===
-show_help() {
- if [ "$SOURCED" = false ]; then
- sed -n '/^# =/,/^# =/p' "$0" | sed 's/^# //g' | sed 's/^=.*//g'
- else
- echo "MeAjudaAi Test Performance Optimization Script"
- echo "Use: ./scripts/optimize.sh --help para ajuda completa"
- fi
-}
-
-# === Funções de Logging ===
-print_header() {
- echo -e "${BLUE}===================================================================${NC}"
- echo -e "${BLUE} $1${NC}"
- echo -e "${BLUE}===================================================================${NC}"
-}
-
-print_info() {
- echo -e "${GREEN}✅ $1${NC}"
-}
-
-print_warning() {
- echo -e "${YELLOW}⚠️ $1${NC}"
-}
-
-print_error() {
- echo -e "${RED}❌ $1${NC}"
-}
-
-print_verbose() {
- if [ "$VERBOSE" = true ]; then
- echo -e "${CYAN}🔍 $1${NC}"
- fi
-}
-
-print_step() {
- echo -e "${BLUE}🔧 $1${NC}"
-}
-
-# === Parsing de argumentos (apenas se não foi sourced) ===
-if [ "$SOURCED" = false ]; then
- while [[ $# -gt 0 ]]; do
- case $1 in
- -h|--help)
- show_help
- exit 0
- ;;
- -v|--verbose)
- VERBOSE=true
- shift
- ;;
- -r|--reset)
- RESET=true
- shift
- ;;
- -t|--test)
- TEST_PERFORMANCE=true
- shift
- ;;
- --docker-only)
- DOCKER_ONLY=true
- shift
- ;;
- --dotnet-only)
- DOTNET_ONLY=true
- shift
- ;;
- *)
- echo "Opção desconhecida: $1"
- show_help
- exit 1
- ;;
- esac
- done
-fi
-
-# === Salvar estado atual (para reset) ===
-save_current_state() {
- if [ "$RESET" = false ]; then
- print_verbose "Salvando estado atual das variáveis..."
-
- # Salvar script de restauração idempotente
- local state_file
- state_file="$(mktemp -t meajudaai_env_backup.XXXXXX)"
- {
- echo "#!/usr/bin/env bash"
- echo "# Gerado em $(date)"
- # Lista completa de variáveis que este script muta
- vars=(DOCKER_HOST TESTCONTAINERS_RYUK_DISABLED TESTCONTAINERS_CHECKS_DISABLE TESTCONTAINERS_WAIT_STRATEGY_RETRIES \
- DOTNET_SYSTEM_GLOBALIZATION_INVARIANT DOTNET_SKIP_FIRST_TIME_EXPERIENCE DOTNET_CLI_TELEMETRY_OPTOUT DOTNET_RUNNING_IN_CONTAINER \
- ASPNETCORE_ENVIRONMENT COMPlus_EnableDiagnostics COMPlus_TieredCompilation DOTNET_TieredCompilation DOTNET_ReadyToRun DOTNET_TC_QuickJitForLoops \
- POSTGRES_SHARED_PRELOAD_LIBRARIES POSTGRES_LOGGING_COLLECTOR POSTGRES_LOG_STATEMENT POSTGRES_LOG_DURATION POSTGRES_LOG_CHECKPOINTS \
- POSTGRES_CHECKPOINT_COMPLETION_TARGET POSTGRES_WAL_BUFFERS POSTGRES_SHARED_BUFFERS POSTGRES_EFFECTIVE_CACHE_SIZE \
- POSTGRES_MAINTENANCE_WORK_MEM POSTGRES_WORK_MEM POSTGRES_FSYNC POSTGRES_SYNCHRONOUS_COMMIT POSTGRES_FULL_PAGE_WRITES)
- for v in "${vars[@]}"; do
- if [ -n "${!v+x}" ]; then
- # shell-escaped export da configuração original
- printf "export %s=%q\n" "$v" "${!v}"
- else
- printf "unset %s\n" "$v"
- fi
- done
- } > "$state_file"
-
- export MEAJUDAAI_ENV_BACKUP="$state_file"
- print_verbose "Estado salvo em: $state_file"
- fi
-}
-
-# === Restaurar estado original ===
-restore_original_state() {
- print_header "Restaurando Estado Original"
-
- if [ -n "${MEAJUDAAI_ENV_BACKUP:-}" ] && [ -f "$MEAJUDAAI_ENV_BACKUP" ]; then
- # Safety checks: only accept files we created under /tmp and not symlinks
- case "$MEAJUDAAI_ENV_BACKUP" in
- /tmp/meajudaai_env_backup.*) ;;
- *)
- print_error "Caminho de backup inválido: $MEAJUDAAI_ENV_BACKUP"
- return 1
- ;;
- esac
- if [ -L "$MEAJUDAAI_ENV_BACKUP" ]; then
- print_error "Backup é um symlink; abortando restauração."
- return 1
- fi
- print_step "Restaurando variáveis originais..."
-
- # Restaurar variáveis exatamente como estavam
- # shellcheck disable=SC1090
- source "$MEAJUDAAI_ENV_BACKUP"
-
- # Limpar arquivo de backup
- rm -f "$MEAJUDAAI_ENV_BACKUP"
- unset MEAJUDAAI_ENV_BACKUP
-
- print_info "Estado original restaurado!"
- else
- print_warning "Nenhum backup encontrado para restaurar"
- fi
-}
-
-# === Otimizações Docker/TestContainers ===
-apply_docker_optimizations() {
- print_step "Aplicando otimizações Docker/TestContainers..."
-
- # Configurações Docker para Windows
- if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]]; then
- if [[ -z "${DOCKER_HOST:-}" ]]; then
- export DOCKER_HOST="npipe://./pipe/docker_engine"
- print_verbose "Docker Host configurado para Windows"
- else
- print_verbose "Mantendo DOCKER_HOST existente: $DOCKER_HOST"
- fi
- fi
-
- # Desabilitar recursos pesados do TestContainers
- export TESTCONTAINERS_RYUK_DISABLED=true
- export TESTCONTAINERS_CHECKS_DISABLE=true
- export TESTCONTAINERS_WAIT_STRATEGY_RETRIES=1
-
- print_verbose "TestContainers otimizado para performance"
- print_info "Otimizações Docker aplicadas (70% mais rápido)"
-}
-
-# === Otimizações .NET Runtime ===
-apply_dotnet_optimizations() {
- print_step "Aplicando otimizações .NET Runtime..."
-
- # Configurações globais .NET
- export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
- export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
- export DOTNET_CLI_TELEMETRY_OPTOUT=1
- export DOTNET_RUNNING_IN_CONTAINER=1
- export ASPNETCORE_ENVIRONMENT=Testing
-
- # Desabilitar diagnósticos para performance
- export COMPlus_EnableDiagnostics=0
- export COMPlus_TieredCompilation=0
- export DOTNET_TieredCompilation=0
- export DOTNET_ReadyToRun=0
- export DOTNET_TC_QuickJitForLoops=1
-
- print_verbose "Runtime .NET otimizado para testes"
- print_info "Otimizações .NET aplicadas (40% menos overhead)"
-}
-
-# === Otimizações PostgreSQL ===
-apply_postgres_optimizations() {
- print_step "Aplicando otimizações PostgreSQL..."
-
- # Configurações de logging (reduzir I/O)
- export POSTGRES_SHARED_PRELOAD_LIBRARIES=""
- export POSTGRES_LOGGING_COLLECTOR=off
- export POSTGRES_LOG_STATEMENT=none
- export POSTGRES_LOG_DURATION=off
- export POSTGRES_LOG_CHECKPOINTS=off
-
- # Configurações de performance
- export POSTGRES_CHECKPOINT_COMPLETION_TARGET=0.9
- export POSTGRES_WAL_BUFFERS=16MB
- export POSTGRES_SHARED_BUFFERS=256MB
- export POSTGRES_EFFECTIVE_CACHE_SIZE=1GB
- export POSTGRES_MAINTENANCE_WORK_MEM=64MB
- export POSTGRES_WORK_MEM=4MB
-
- # Configurações agressivas para testes (não usar em produção!)
- export POSTGRES_FSYNC=off
- export POSTGRES_SYNCHRONOUS_COMMIT=off
- export POSTGRES_FULL_PAGE_WRITES=off
-
- print_verbose "PostgreSQL configurado para máxima performance em testes"
- print_warning "⚠️ Configurações de PostgreSQL são apenas para TESTES!"
- print_info "Otimizações PostgreSQL aplicadas (60% mais rápido setup)"
-}
-
-# === Aplicar todas as otimizações ===
-apply_all_optimizations() {
- print_header "Aplicando Otimizações de Performance"
-
- save_current_state
-
- if [ "$DOTNET_ONLY" = false ]; then
- apply_docker_optimizations
- apply_postgres_optimizations
- fi
-
- if [ "$DOCKER_ONLY" = false ]; then
- apply_dotnet_optimizations
- fi
-
- print_header "Resumo das Otimizações"
- print_info "🚀 Melhorias esperadas:"
- print_info " • Docker/TestContainers: 70% mais rápido"
- print_info " • .NET Runtime: 40% menos overhead"
- print_info " • PostgreSQL: 60% setup mais rápido"
- print_info " • Tempo total: ~6-8s (vs ~20-25s padrão)"
- print_info ""
- print_info "💡 Para usar as otimizações:"
- print_info " dotnet test --configuration Release --verbosity minimal"
- print_info ""
- print_info "🔄 Para restaurar configurações:"
- print_info " ./scripts/optimize.sh --reset"
-}
-
-# === Teste de Performance ===
-run_performance_test() {
- print_header "Executando Teste de Performance"
-
- cd "$PROJECT_ROOT" || {
- print_error "Falha ao mudar para diretório do projeto: $PROJECT_ROOT"
- return 1
- }
-
- print_step "Executando testes com otimizações..."
- local start_time
- start_time=$(date +%s)
-
- if ! dotnet test --configuration Release --verbosity minimal --nologo --filter "Category!=E2E"; then
- print_warning "Alguns testes falharam durante o teste de performance."
- fi
-
- local end_time
- local duration
- end_time=$(date +%s)
- duration=$((end_time - start_time))
-
- print_header "Resultado do Teste de Performance"
- print_info "Tempo de execução: ${duration}s"
-
- if [ "$duration" -lt 10 ]; then
- print_info "🎉 Excelente! Performance otimizada alcançada"
- elif [ "$duration" -lt 15 ]; then
- print_info "👍 Boa performance, dentro do esperado"
- else
- print_warning "⚠️ Performance abaixo do esperado (>15s)"
- print_info "Considere verificar configurações do Docker e specs da máquina"
- fi
-}
-
-# === Verificar estado atual ===
-show_current_state() {
- print_header "Estado Atual das Otimizações"
-
- echo "🔍 Variáveis de ambiente relevantes:"
- echo ""
-
- # Docker
- echo "📦 Docker:"
- echo " DOCKER_HOST: ${DOCKER_HOST:-'(padrão)'}"
- echo " TESTCONTAINERS_RYUK_DISABLED: ${TESTCONTAINERS_RYUK_DISABLED:-'false'}"
- echo ""
-
- # .NET
- echo "⚙️ .NET:"
- echo " DOTNET_RUNNING_IN_CONTAINER: ${DOTNET_RUNNING_IN_CONTAINER:-'false'}"
- echo " ASPNETCORE_ENVIRONMENT: ${ASPNETCORE_ENVIRONMENT:-'(padrão)'}"
- echo " COMPlus_EnableDiagnostics: ${COMPlus_EnableDiagnostics:-'1'}"
- echo ""
-
- # PostgreSQL
- echo "🐘 PostgreSQL:"
- echo " POSTGRES_FSYNC: ${POSTGRES_FSYNC:-'on'}"
- echo " POSTGRES_SYNCHRONOUS_COMMIT: ${POSTGRES_SYNCHRONOUS_COMMIT:-'on'}"
- echo ""
-
- if [ -n "${MEAJUDAAI_ENV_BACKUP:-}" ]; then
- print_info "✅ Backup disponível para restauração"
- else
- print_warning "⚠️ Nenhum backup disponível"
- fi
-}
-
-# === Execução Principal ===
-main() {
- if [ "$RESET" = true ]; then
- restore_original_state
- return 0
- fi
-
- apply_all_optimizations
-
- if [ "$TEST_PERFORMANCE" = true ]; then
- run_performance_test
- fi
-
- if [ "$VERBOSE" = true ]; then
- show_current_state
- fi
-
- if [ "$SOURCED" = true ]; then
- print_info "Otimizações aplicadas no shell atual!"
- print_info "Execute testes normalmente para usar as otimizações."
- fi
-}
-
-# === Execução (apenas se não foi sourced) ===
-if [ "$SOURCED" = false ]; then
- main "$@"
-else
- # Se foi sourced, aplicar otimizações silenciosamente
- save_current_state
- apply_docker_optimizations > /dev/null 2>&1
- apply_dotnet_optimizations > /dev/null 2>&1
- apply_postgres_optimizations > /dev/null 2>&1
- echo "🚀 Otimizações aplicadas! Use 'optimize.sh --reset' para restaurar."
-fi
\ No newline at end of file
diff --git a/scripts/seed-dev-data.ps1 b/scripts/seed-dev-data.ps1
new file mode 100644
index 000000000..d1e66cc03
--- /dev/null
+++ b/scripts/seed-dev-data.ps1
@@ -0,0 +1,233 @@
+#requires -Version 7.0
+<#
+.SYNOPSIS
+ Seed inicial de dados para ambiente de desenvolvimento
+
+.DESCRIPTION
+ Popula o banco de dados com dados iniciais para desenvolvimento e testes:
+ - Categorias de serviços
+ - Serviços básicos
+ - Cidades permitidas
+ - Usuários de teste
+ - Providers de exemplo
+
+.PARAMETER Environment
+ Ambiente alvo (Development, Staging). Default: Development
+
+.EXAMPLE
+ .\seed-dev-data.ps1
+
+.EXAMPLE
+ .\seed-dev-data.ps1 -Environment Staging
+#>
+
+[CmdletBinding()]
+param(
+ [Parameter()]
+ [ValidateSet('Development', 'Staging')]
+ [string]$Environment = 'Development',
+
+ [Parameter()]
+ [string]$ApiBaseUrl = 'http://localhost:5000'
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+# Cores para output
+function Write-Success { param($Message) Write-Host "✅ $Message" -ForegroundColor Green }
+function Write-Info { param($Message) Write-Host "ℹ️ $Message" -ForegroundColor Cyan }
+function Write-Warning { param($Message) Write-Host "⚠️ $Message" -ForegroundColor Yellow }
+function Write-Error { param($Message) Write-Host "❌ $Message" -ForegroundColor Red }
+
+Write-Host "🌱 Seed de Dados - MeAjudaAi [$Environment]" -ForegroundColor Cyan
+Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
+Write-Host ""
+
+# Verificar se API está rodando
+Write-Info "Verificando API em $ApiBaseUrl..."
+try {
+ $health = Invoke-RestMethod -Uri "$ApiBaseUrl/health" -Method Get -TimeoutSec 5
+ Write-Success "API está rodando"
+} catch {
+ Write-Error "API não está acessível em $ApiBaseUrl"
+ Write-Host "Inicie a API primeiro: cd src/Bootstrapper/MeAjudaAi.ApiService && dotnet run" -ForegroundColor Yellow
+ exit 1
+}
+
+# Obter token de autenticação
+Write-Info "Obtendo token de autenticação..."
+$keycloakUrl = "http://localhost:8080"
+$tokenParams = @{
+ Uri = "$keycloakUrl/realms/meajudaai/protocol/openid-connect/token"
+ Method = 'Post'
+ ContentType = 'application/x-www-form-urlencoded'
+ Body = @{
+ client_id = 'meajudaai-api'
+ username = 'admin'
+ password = 'admin123'
+ grant_type = 'password'
+ }
+}
+
+try {
+ $tokenResponse = Invoke-RestMethod @tokenParams
+ $token = $tokenResponse.access_token
+ Write-Success "Token obtido com sucesso"
+} catch {
+ Write-Error "Falha ao obter token do Keycloak"
+ Write-Host "Verifique se Keycloak está rodando: docker-compose up keycloak" -ForegroundColor Yellow
+ exit 1
+}
+
+$headers = @{
+ 'Authorization' = "Bearer $token"
+ 'Content-Type' = 'application/json'
+ 'Api-Version' = '1.0'
+}
+
+Write-Host ""
+Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
+Write-Host "📦 Seeding: ServiceCatalogs" -ForegroundColor Yellow
+Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
+
+# Categorias
+$categories = @(
+ @{ name = "Saúde"; description = "Serviços relacionados à saúde e bem-estar" }
+ @{ name = "Educação"; description = "Serviços educacionais e de capacitação" }
+ @{ name = "Assistência Social"; description = "Programas de assistência e suporte social" }
+ @{ name = "Jurídico"; description = "Serviços jurídicos e advocatícios" }
+ @{ name = "Habitação"; description = "Moradia e programas habitacionais" }
+ @{ name = "Alimentação"; description = "Programas de segurança alimentar" }
+)
+
+$categoryIds = @{}
+
+foreach ($cat in $categories) {
+ Write-Info "Criando categoria: $($cat.name)"
+ try {
+ $response = Invoke-RestMethod -Uri "$ApiBaseUrl/api/v1/catalogs/admin/categories" `
+ -Method Post `
+ -Headers $headers `
+ -Body ($cat | ConvertTo-Json -Depth 10)
+
+ $categoryIds[$cat.name] = $response.id
+ Write-Success "Categoria '$($cat.name)' criada (ID: $($response.id))"
+ } catch {
+ if ($_.Exception.Response.StatusCode -eq 409) {
+ Write-Warning "Categoria '$($cat.name)' já existe"
+ } else {
+ Write-Error "Erro ao criar categoria '$($cat.name)': $_"
+ }
+ }
+}
+
+# Serviços
+if ($categoryIds.Count -gt 0) {
+ $services = @(
+ @{
+ name = "Atendimento Psicológico Gratuito"
+ description = "Atendimento psicológico individual ou em grupo"
+ categoryId = $categoryIds["Saúde"]
+ eligibilityCriteria = "Renda familiar até 3 salários mínimos"
+ requiredDocuments = @("RG", "CPF", "Comprovante de residência", "Comprovante de renda")
+ }
+ @{
+ name = "Curso de Informática Básica"
+ description = "Curso gratuito de informática e inclusão digital"
+ categoryId = $categoryIds["Educação"]
+ eligibilityCriteria = "Jovens de 14 a 29 anos"
+ requiredDocuments = @("RG", "CPF", "Comprovante de escolaridade")
+ }
+ @{
+ name = "Cesta Básica"
+ description = "Distribuição mensal de cestas básicas"
+ categoryId = $categoryIds["Alimentação"]
+ eligibilityCriteria = "Famílias em situação de vulnerabilidade"
+ requiredDocuments = @("Cadastro único", "Comprovante de residência")
+ }
+ @{
+ name = "Orientação Jurídica Gratuita"
+ description = "Atendimento jurídico para questões civis e trabalhistas"
+ categoryId = $categoryIds["Jurídico"]
+ eligibilityCriteria = "Renda familiar até 2 salários mínimos"
+ requiredDocuments = @("RG", "CPF", "Documentos relacionados ao caso")
+ }
+ )
+
+ foreach ($service in $services) {
+ if ($service.categoryId) {
+ Write-Info "Criando serviço: $($service.name)"
+ try {
+ $response = Invoke-RestMethod -Uri "$ApiBaseUrl/api/v1/catalogs/admin/services" `
+ -Method Post `
+ -Headers $headers `
+ -Body ($service | ConvertTo-Json -Depth 10)
+
+ Write-Success "Serviço '$($service.name)' criado"
+ } catch {
+ if ($_.Exception.Response.StatusCode -eq 409) {
+ Write-Warning "Serviço '$($service.name)' já existe"
+ } else {
+ Write-Error "Erro ao criar serviço '$($service.name)': $_"
+ }
+ }
+ }
+ }
+}
+
+Write-Host ""
+Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
+Write-Host "📍 Seeding: Locations (AllowedCities)" -ForegroundColor Yellow
+Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
+
+$allowedCities = @(
+ @{ ibgeCode = "3550308"; cityName = "São Paulo"; state = "SP"; isActive = $true }
+ @{ ibgeCode = "3304557"; cityName = "Rio de Janeiro"; state = "RJ"; isActive = $true }
+ @{ ibgeCode = "3106200"; cityName = "Belo Horizonte"; state = "MG"; isActive = $true }
+ @{ ibgeCode = "4106902"; cityName = "Curitiba"; state = "PR"; isActive = $true }
+ @{ ibgeCode = "4314902"; cityName = "Porto Alegre"; state = "RS"; isActive = $true }
+ @{ ibgeCode = "5300108"; cityName = "Brasília"; state = "DF"; isActive = $true }
+ @{ ibgeCode = "2927408"; cityName = "Salvador"; state = "BA"; isActive = $true }
+ @{ ibgeCode = "2304400"; cityName = "Fortaleza"; state = "CE"; isActive = $true }
+ @{ ibgeCode = "2611606"; cityName = "Recife"; state = "PE"; isActive = $true }
+ @{ ibgeCode = "1302603"; cityName = "Manaus"; state = "AM"; isActive = $true }
+)
+
+foreach ($city in $allowedCities) {
+ Write-Info "Adicionando cidade: $($city.cityName)/$($city.state)"
+ try {
+ $response = Invoke-RestMethod -Uri "$ApiBaseUrl/api/v1/locations/admin/allowed-cities" `
+ -Method Post `
+ -Headers $headers `
+ -Body ($city | ConvertTo-Json -Depth 10)
+
+ Write-Success "Cidade '$($city.cityName)/$($city.state)' adicionada"
+ } catch {
+ if ($_.Exception.Response.StatusCode -eq 409) {
+ Write-Warning "Cidade '$($city.cityName)/$($city.state)' já existe"
+ } else {
+ Write-Error "Erro ao adicionar cidade '$($city.cityName)/$($city.state)': $_"
+ }
+ }
+}
+
+Write-Host ""
+Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
+Write-Host "🎉 Seed Concluído!" -ForegroundColor Green
+Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
+Write-Host ""
+Write-Host "📊 Dados inseridos:" -ForegroundColor Cyan
+# Computar contagens seguras para evitar referência a variáveis indefinidas
+$categoryCount = if ($categories) { $categories.Count } else { 0 }
+$serviceCount = if ($services) { $services.Count } else { 0 }
+$cityCount = if ($allowedCities) { $allowedCities.Count } else { 0 }
+Write-Host " • Categorias: $categoryCount" -ForegroundColor White
+Write-Host " • Serviços: $serviceCount" -ForegroundColor White
+Write-Host " • Cidades: $cityCount" -ForegroundColor White
+Write-Host ""
+Write-Host "💡 Próximos passos:" -ForegroundColor Cyan
+Write-Host " 1. Cadastrar providers usando Bruno collections" -ForegroundColor White
+Write-Host " 2. Indexar providers para busca" -ForegroundColor White
+Write-Host " 3. Testar endpoints de busca" -ForegroundColor White
+Write-Host ""
diff --git a/scripts/setup.sh b/scripts/setup.sh
deleted file mode 100644
index 205eb2dc2..000000000
--- a/scripts/setup.sh
+++ /dev/null
@@ -1,452 +0,0 @@
-#!/bin/bash
-
-# =============================================================================
-# MeAjudaAi Project Setup Script - Onboarding para Novos Desenvolvedores
-# =============================================================================
-# Script completo para configuração inicial do ambiente de desenvolvimento.
-# Instala dependências, configura ferramentas e prepara o ambiente local.
-#
-# Uso:
-# ./scripts/setup.sh [opções]
-#
-# Opções:
-# -h, --help Mostra esta ajuda
-# -v, --verbose Modo verboso
-# -s, --skip-install Pula instalação de dependências
-# -f, --force Força reinstalação mesmo se já existir
-# --dev-only Apenas dependências de desenvolvimento
-# --no-docker Pula verificação e setup do Docker
-# --no-azure Pula setup do Azure CLI
-#
-# Exemplos:
-# ./scripts/setup.sh # Setup completo
-# ./scripts/setup.sh --dev-only # Apenas ferramentas de dev
-# ./scripts/setup.sh --no-docker # Sem Docker
-#
-# Dependências que serão verificadas/instaladas:
-# - .NET 8 SDK
-# - Docker Desktop
-# - Azure CLI
-# - Git
-# - Visual Studio Code (opcional)
-# - PowerShell (Windows)
-# =============================================================================
-
-set -e # Para em caso de erro
-
-# === Configurações ===
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
-
-# === Variáveis de Controle ===
-VERBOSE=false
-SKIP_INSTALL=false
-FORCE=false
-DEV_ONLY=false
-NO_DOCKER=false
-NO_AZURE=false
-
-# === Cores para output ===
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-# === Função de ajuda ===
-show_help() {
- sed -n '/^# =/,/^# =/p' "$0" | sed 's/^# //g' | sed 's/^=.*//g'
-}
-
-# === Funções de Logging ===
-print_header() {
- echo -e "${BLUE}===================================================================${NC}"
- echo -e "${BLUE} $1${NC}"
- echo -e "${BLUE}===================================================================${NC}"
-}
-
-print_info() {
- echo -e "${GREEN}✅ $1${NC}"
-}
-
-print_warning() {
- echo -e "${YELLOW}⚠️ $1${NC}"
-}
-
-print_error() {
- echo -e "${RED}❌ $1${NC}"
-}
-
-print_verbose() {
- if [ "$VERBOSE" = true ]; then
- echo -e "${CYAN}🔍 $1${NC}"
- fi
-}
-
-print_step() {
- echo -e "${BLUE}🔧 $1${NC}"
-}
-
-# === Parsing de argumentos ===
-while [[ $# -gt 0 ]]; do
- case $1 in
- -h|--help)
- show_help
- exit 0
- ;;
- -v|--verbose)
- VERBOSE=true
- shift
- ;;
- -s|--skip-install)
- SKIP_INSTALL=true
- shift
- ;;
- -f|--force)
- FORCE=true
- shift
- ;;
- --dev-only)
- DEV_ONLY=true
- shift
- ;;
- --no-docker)
- NO_DOCKER=true
- shift
- ;;
- --no-azure)
- NO_AZURE=true
- shift
- ;;
- *)
- echo "Opção desconhecida: $1"
- show_help
- exit 1
- ;;
- esac
-done
-
-# === Navegar para raiz do projeto ===
-cd "$PROJECT_ROOT"
-
-# === Detectar Sistema Operacional ===
-detect_os() {
- if [[ "$OSTYPE" == "linux-gnu"* ]]; then
- OS="linux"
- DISTRO=$(lsb_release -si 2>/dev/null || echo "Unknown")
- elif [[ "$OSTYPE" == "darwin"* ]]; then
- OS="macos"
- elif [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
- OS="windows"
- else
- OS="unknown"
- fi
-
- print_verbose "Sistema operacional detectado: $OS"
-}
-
-# === Verificar se comando existe ===
-command_exists() {
- command -v "$1" &> /dev/null
-}
-
-# === Verificar e instalar .NET ===
-setup_dotnet() {
- print_step "Verificando .NET SDK..."
-
- if command_exists dotnet; then
- local dotnet_version=$(dotnet --version)
- print_info ".NET SDK já instalado: $dotnet_version"
-
- # Verificar se é versão 8.x
- if [[ $dotnet_version == 8.* ]]; then
- print_info "Versão .NET 8 detectada ✓"
- else
- print_warning "Versão .NET $dotnet_version detectada. Recomendado: 8.x"
- fi
- else
- if [ "$SKIP_INSTALL" = true ]; then
- print_error ".NET SDK não encontrado e instalação foi pulada"
- return 1
- fi
-
- print_warning ".NET SDK não encontrado. Instalando..."
-
- case $OS in
- "windows")
- print_info "Baixe e instale o .NET 8 SDK de: https://dotnet.microsoft.com/download"
- print_warning "Reinicie o terminal após a instalação"
- ;;
- "macos")
- if command_exists brew; then
- brew install --cask dotnet
- else
- print_info "Instale o Homebrew e execute: brew install --cask dotnet"
- fi
- ;;
- "linux")
- print_info "Para Ubuntu/Debian:"
- print_info " wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb"
- print_info " sudo dpkg -i packages-microsoft-prod.deb"
- print_info " sudo apt-get update && sudo apt-get install -y dotnet-sdk-8.0"
- ;;
- esac
- fi
-}
-
-# === Verificar e configurar Git ===
-setup_git() {
- print_step "Verificando Git..."
-
- if command_exists git; then
- local git_version=$(git --version)
- print_info "Git já instalado: $git_version"
-
- # Verificar configuração
- local git_name=$(git config --global user.name 2>/dev/null || echo "")
- local git_email=$(git config --global user.email 2>/dev/null || echo "")
-
- if [ -z "$git_name" ] || [ -z "$git_email" ]; then
- print_warning "Configuração do Git incompleta"
- print_info "Configure com: git config --global user.name 'Seu Nome'"
- print_info "Configure com: git config --global user.email 'seu@email.com'"
- else
- print_info "Git configurado para: $git_name <$git_email>"
- fi
- else
- print_error "Git não encontrado. Instale o Git primeiro."
- case $OS in
- "windows")
- print_info "Baixe de: https://git-scm.com/download/win"
- ;;
- "macos")
- print_info "Execute: brew install git"
- ;;
- "linux")
- print_info "Execute: sudo apt-get install git"
- ;;
- esac
- return 1
- fi
-}
-
-# === Verificar e configurar Docker ===
-setup_docker() {
- if [ "$NO_DOCKER" = true ]; then
- print_warning "Setup do Docker foi pulado"
- return 0
- fi
-
- print_step "Verificando Docker..."
-
- if command_exists docker; then
- print_info "Docker já instalado"
-
- # Verificar se está rodando
- if docker info &> /dev/null; then
- print_info "Docker está rodando ✓"
- else
- print_warning "Docker está instalado mas não está rodando"
- print_info "Inicie o Docker Desktop"
- fi
- else
- if [ "$SKIP_INSTALL" = true ]; then
- print_warning "Docker não encontrado e instalação foi pulada"
- return 0
- fi
-
- print_warning "Docker não encontrado"
- case $OS in
- "windows"|"macos")
- print_info "Baixe e instale o Docker Desktop de: https://www.docker.com/products/docker-desktop"
- ;;
- "linux")
- print_info "Para Ubuntu/Debian:"
- print_info " curl -fsSL https://get.docker.com -o get-docker.sh"
- print_info " sudo sh get-docker.sh"
- print_info " sudo usermod -aG docker \$USER"
- ;;
- esac
- fi
-}
-
-# === Verificar e configurar Azure CLI ===
-setup_azure_cli() {
- if [ "$NO_AZURE" = true ] || [ "$DEV_ONLY" = true ]; then
- print_warning "Setup do Azure CLI foi pulado"
- return 0
- fi
-
- print_step "Verificando Azure CLI..."
-
- if command_exists az; then
- local az_version=$(az --version 2>/dev/null | head -n1 || echo "Unknown")
- print_info "Azure CLI já instalado: $az_version"
-
- # Verificar autenticação
- if az account show &> /dev/null; then
- local subscription=$(az account show --query name -o tsv)
- print_info "Autenticado na subscription: $subscription"
- else
- print_warning "Azure CLI não está autenticado"
- print_info "Execute: az login"
- fi
- else
- if [ "$SKIP_INSTALL" = true ]; then
- print_warning "Azure CLI não encontrado e instalação foi pulada"
- return 0
- fi
-
- print_warning "Azure CLI não encontrado"
- case $OS in
- "windows")
- print_info "Baixe de: https://aka.ms/installazurecliwindows"
- ;;
- "macos")
- print_info "Execute: brew install azure-cli"
- ;;
- "linux")
- print_info "Execute: curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash"
- ;;
- esac
- fi
-}
-
-# === Configurar Visual Studio Code ===
-setup_vscode() {
- print_step "Verificando Visual Studio Code..."
-
- if command_exists code; then
- print_info "Visual Studio Code já instalado"
-
- # Sugerir extensões úteis
- print_info "Extensões recomendadas para o projeto:"
- print_info " - C# (ms-dotnettools.csharp)"
- print_info " - Docker (ms-azuretools.vscode-docker)"
- print_info " - Azure Tools (ms-vscode.vscode-node-azure-pack)"
- print_info " - REST Client (humao.rest-client)"
- print_info " - GitLens (eamodio.gitlens)"
- else
- print_warning "Visual Studio Code não encontrado (opcional)"
- print_info "Baixe de: https://code.visualstudio.com/"
- fi
-}
-
-# === Configurar ambiente do projeto ===
-setup_project_environment() {
- print_step "Configurando ambiente do projeto..."
-
- # Restaurar dependências
- print_info "Restaurando dependências .NET..."
- dotnet restore
-
- # Verificar se build funciona
- print_info "Testando build inicial..."
- dotnet build --configuration Debug --verbosity minimal
-
- if [ $? -eq 0 ]; then
- print_info "Build inicial bem-sucedido ✓"
- else
- print_error "Falha no build inicial"
- return 1
- fi
-
- # Configurar user secrets (se necessário)
- print_info "Configurando user secrets..."
- local api_project="src/Bootstrapper/MeAjudaAi.ApiService"
- if [ -d "$api_project" ]; then
- cd "$api_project"
- dotnet user-secrets init 2>/dev/null || true
- cd "$PROJECT_ROOT"
- print_info "User secrets inicializados"
- fi
-
- # Criar arquivos de configuração local se não existirem
- local env_example=".env.example"
- local env_local=".env.local"
-
- if [ -f "$env_example" ] && [ ! -f "$env_local" ]; then
- cp "$env_example" "$env_local"
- print_info "Arquivo .env.local criado a partir do exemplo"
- print_warning "Edite .env.local com suas configurações específicas"
- fi
-}
-
-# === Executar testes básicos ===
-run_basic_tests() {
- print_step "Executando testes básicos..."
-
- if [ -d "tests" ]; then
- print_info "Executando testes unitários básicos..."
- dotnet test --configuration Debug --verbosity minimal --filter "Category!=Integration&Category!=E2E"
-
- if [ $? -eq 0 ]; then
- print_info "Testes básicos passaram ✓"
- else
- print_warning "Alguns testes básicos falharam"
- fi
- else
- print_info "Nenhum projeto de teste encontrado"
- fi
-}
-
-# === Mostrar próximos passos ===
-show_next_steps() {
- print_header "Próximos Passos"
-
- echo "🎉 Setup concluído! Aqui estão os próximos passos:"
- echo ""
- echo "📋 Comandos úteis:"
- echo " ./scripts/dev.sh # Executar em modo desenvolvimento"
- echo " ./scripts/test.sh # Executar todos os testes"
- echo " ./scripts/deploy.sh dev # Deploy para ambiente dev"
- echo ""
- echo "🔧 Configurações adicionais:"
- echo " - Edite .env.local com suas configurações"
- echo " - Configure user secrets: dotnet user-secrets set \"key\" \"value\""
- echo " - Autentique no Azure: az login"
- echo ""
- echo "📚 Documentação:"
- echo " - README.md do projeto"
- echo " - scripts/README.md"
- echo " - docs/ (se disponível)"
- echo ""
- echo "🆘 Problemas? Execute novamente com --verbose para mais detalhes"
-}
-
-# === Execução Principal ===
-main() {
- local start_time=$(date +%s)
-
- print_header "MeAjudaAi Project Setup"
- print_info "Configurando ambiente de desenvolvimento..."
-
- detect_os
-
- # Verificar dependências essenciais
- setup_git
- setup_dotnet
-
- # Verificar ferramentas opcionais
- setup_docker
- setup_azure_cli
- setup_vscode
-
- # Configurar projeto
- setup_project_environment
- run_basic_tests
-
- local end_time=$(date +%s)
- local duration=$((end_time - start_time))
-
- print_header "Setup Concluído"
- print_info "Tempo total: ${duration}s"
-
- show_next_steps
-
- print_info "Ambiente pronto para desenvolvimento! 🚀"
-}
-
-# === Execução ===
-main "$@"
\ No newline at end of file
diff --git a/scripts/test-coverage-like-pipeline.ps1 b/scripts/test-coverage-like-pipeline.ps1
deleted file mode 100644
index 0fda4a8f0..000000000
--- a/scripts/test-coverage-like-pipeline.ps1
+++ /dev/null
@@ -1,121 +0,0 @@
-# Script para executar testes com cobertura igual à pipeline
-# Gera relatórios no formato OpenCover e aplica os mesmos filtros
-
-Write-Host "🧪 Executando testes com cobertura (formato pipeline)..." -ForegroundColor Cyan
-
-# Limpar cobertura anterior
-if (Test-Path "coverage") {
- Remove-Item "coverage" -Recurse -Force
-}
-
-# Filtros iguais à pipeline
-$EXCLUDE_BY_FILE = "**/*OpenApi*.generated.cs,**/System.Runtime.CompilerServices*.cs,**/*RegexGenerator.g.cs"
-$EXCLUDE_BY_ATTRIBUTE = "Obsolete,GeneratedCode,CompilerGenerated"
-$EXCLUDE_FILTER = "[*.Tests]*,[*.Tests.*]*,[*Test*]*,[testhost]*"
-$INCLUDE_FILTER = "[MeAjudaAi*]*"
-
-# Criar runsettings temporário
-$runsettings = @"
-
-
-
-
-
-
- opencover
- $EXCLUDE_BY_FILE
- $EXCLUDE_BY_ATTRIBUTE
- $EXCLUDE_FILTER
- $INCLUDE_FILTER
-
-
-
-
-
-"@
-
-$runsettingsFile = "coverage.runsettings"
-$runsettings | Out-File -FilePath $runsettingsFile -Encoding UTF8
-
-Write-Host "`n📦 Executando testes com cobertura OpenCover..." -ForegroundColor Yellow
-
-dotnet test --configuration Release `
- --collect:"XPlat Code Coverage" `
- --results-directory ./coverage `
- --settings $runsettingsFile `
- --verbosity minimal
-
-# Verificar arquivos gerados
-Write-Host "`n📊 Arquivos de cobertura gerados:" -ForegroundColor Cyan
-Get-ChildItem -Path "coverage" -Filter "*.xml" -Recurse | ForEach-Object {
- Write-Host " ✅ $($_.FullName)" -ForegroundColor Green
-}
-
-# Gerar relatório agregado com filtros da pipeline
-Write-Host "`n🔗 Gerando relatório agregado com filtros da pipeline..." -ForegroundColor Cyan
-
-# Instalar/atualizar ReportGenerator
-dotnet tool install --global dotnet-reportgenerator-globaltool 2>$null
-dotnet tool update --global dotnet-reportgenerator-globaltool 2>$null
-
-# Filtros da pipeline
-$INCLUDE_ASSEMBLY = "+MeAjudaAi.Modules.Users.*;+MeAjudaAi.Modules.Providers.*;+MeAjudaAi.Modules.Documents.*;+MeAjudaAi.Modules.ServiceCatalogs.*;+MeAjudaAi.Modules.Locations.*;+MeAjudaAi.Modules.SearchProviders.*;+MeAjudaAi.Shared*;+MeAjudaAi.ApiService*"
-$EXCLUDE_CLASS = "-*.Tests;-*.Tests.*;-*Test*;-testhost;-xunit*;-*.Migrations.*;-*.Contracts;-*.Database;-*.Keycloak.*;-*.Monitoring.*;-*NoOp*;-*RabbitMq*;-*ServiceBus*;-*Hangfire*;-*.Jobs.*;-*Options;-*BaseDesignTimeDbContextFactory*;-*SchemaPermissionsManager;-*SimpleHostEnvironment;-*CacheWarmupService;-*GeoPointConverter;-*ModuleNames;-*ModuleApiInfo;-*MessagingExtensions;-*ICacheableQuery"
-
-reportgenerator `
- -reports:"coverage/**/*.xml" `
- -targetdir:"coverage/report-pipeline" `
- -reporttypes:"TextSummary;HtmlSummary;Cobertura" `
- -assemblyfilters:"$INCLUDE_ASSEMBLY" `
- -classfilters:"$EXCLUDE_CLASS" `
- -filefilters:"-*Migrations*"
-
-if (Test-Path "coverage/report-pipeline/Summary.txt") {
- Write-Host "`n📈 COBERTURA COM FILTROS DA PIPELINE:" -ForegroundColor Green
- Write-Host "======================================`n" -ForegroundColor Green
- Get-Content "coverage/report-pipeline/Summary.txt" | Select-Object -First 20
-
- # Extrair porcentagem de linha
- $summaryContent = Get-Content "coverage/report-pipeline/Summary.txt"
- $lineCoverage = ($summaryContent | Select-String "Line coverage:").ToString() -replace '.*Line coverage:\s*', ''
- Write-Host "`n🎯 Line Coverage (igual pipeline): $lineCoverage" -ForegroundColor Cyan
- Write-Host "🎯 Meta: 90%" -ForegroundColor Yellow
-
- # Abrir relatório HTML (cross-platform)
- $htmlReport = "coverage/report-pipeline/summary.html"
- if (Test-Path $htmlReport) {
- Write-Host "`n🌐 Abrindo relatório HTML..." -ForegroundColor Cyan
- $fullPath = Resolve-Path $htmlReport
-
- try {
- if ($IsWindows -or (-not (Test-Path variable:IsWindows))) {
- Start-Process $fullPath
- }
- elseif ($IsMacOS) {
- & open $fullPath
- }
- elseif ($IsLinux) {
- if (Get-Command xdg-open -ErrorAction SilentlyContinue) {
- & xdg-open $fullPath
- }
- elseif (Get-Command sensible-browser -ErrorAction SilentlyContinue) {
- & sensible-browser $fullPath
- }
- else {
- Write-Host "⚠️ Não foi possível abrir automaticamente. Relatório em: $fullPath" -ForegroundColor Yellow
- }
- }
- }
- catch {
- Write-Host "⚠️ Erro ao abrir relatório: $_" -ForegroundColor Yellow
- Write-Host "📄 Relatório disponível em: $fullPath" -ForegroundColor Cyan
- }
- }
-} else {
- Write-Host "`n❌ Falha ao gerar relatório agregado" -ForegroundColor Red
-}
-
-# Limpar runsettings
-Remove-Item $runsettingsFile -ErrorAction SilentlyContinue
-
-Write-Host "`n✅ Análise completa!" -ForegroundColor Green
diff --git a/scripts/test.sh b/scripts/test.sh
deleted file mode 100644
index cff6dce37..000000000
--- a/scripts/test.sh
+++ /dev/null
@@ -1,643 +0,0 @@
-#!/bin/bash
-
-# =============================================================================
-# MeAjudaAi Test Runner Script - Execução Abrangente de Testes
-# =============================================================================
-# Script consolidado para execução de diferentes tipos de testes da aplicação.
-# Inclui testes unitários, de integração, E2E e otimizações de performance.
-#
-# Uso:
-# ./scripts/test.sh [opções]
-#
-# Opções:
-# -h, --help Mostra esta ajuda
-# -v, --verbose Modo verboso
-# -u, --unit Apenas testes unitários
-# -i, --integration Apenas testes de integração
-# -e, --e2e Apenas testes E2E
-# -f, --fast Modo rápido (com otimizações)
-# -c, --coverage Gera relatório de cobertura
-# --skip-build Pula o build
-# --parallel Executa testes em paralelo usando runsettings
-#
-# Exemplos:
-# ./scripts/test.sh # Todos os testes
-# ./scripts/test.sh --unit # Apenas unitários
-# ./scripts/test.sh --fast # Modo otimizado
-# ./scripts/test.sh --coverage # Com cobertura
-# ./scripts/test.sh --parallel # Paralelo via runsettings
-#
-# Arquivos de Configuração:
-# tests/parallel.runsettings # Configuração para execução paralela
-# tests/sequential.runsettings # Configuração para execução sequencial
-#
-# Dependências:
-# - .NET 9.0.x SDK
-# - Docker Desktop (para testes de integração)
-# - reportgenerator (para cobertura)
-# =============================================================================
-
-set -e -u -o pipefail # Pare em caso de erro, variáveis não definidas e falhas em pipelines
-
-# === Configurações ===
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
-TEST_RESULTS_DIR="$PROJECT_ROOT/TestResults"
-COVERAGE_DIR="$PROJECT_ROOT/TestResults/Coverage"
-
-# === Variáveis de Controle ===
-VERBOSE=false
-UNIT_ONLY=false
-INTEGRATION_ONLY=false
-E2E_ONLY=false
-FAST_MODE=false
-COVERAGE=false
-SKIP_BUILD=false
-PARALLEL=false
-DOCKER_AVAILABLE=false
-
-# === Configuração padrão ===
-# Set default configuration if not provided via environment
-CONFIG=${CONFIG:-Release}
-
-# === Cores para output ===
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-NC='\033[0m' # No Color
-
-# === Função de ajuda ===
-show_help() {
- cat <<'USAGE'
-MeAjudaAi Test Runner
-Usage: ./scripts/test.sh [options]
- -h, --help Show this help
- -v, --verbose Verbose logs
- -u, --unit Unit tests only
- -i, --integration Integration tests only
- -e, --e2e E2E tests only
- -f, --fast Apply performance optimizations
- -c, --coverage Generate coverage report
- --skip-build Skip build
- --parallel Run tests in parallel using MSBuild properties
-USAGE
-}
-
-# === Funções de Logging ===
-print_header() {
- echo -e "${BLUE}===================================================================${NC}"
- echo -e "${BLUE} $1${NC}"
- echo -e "${BLUE}===================================================================${NC}"
-}
-
-print_info() {
- echo -e "${GREEN}✅ $1${NC}"
-}
-
-print_warning() {
- echo -e "${YELLOW}⚠️ $1${NC}"
-}
-
-print_error() {
- echo -e "${RED}❌ $1${NC}"
-}
-
-print_verbose() {
- if [ "$VERBOSE" = true ]; then
- echo -e "${CYAN}🔍 $1${NC}"
- fi
-}
-
-# === Parsing de argumentos ===
-while [[ $# -gt 0 ]]; do
- case $1 in
- -h|--help)
- show_help
- exit 0
- ;;
- -v|--verbose)
- VERBOSE=true
- shift
- ;;
- -u|--unit)
- UNIT_ONLY=true
- shift
- ;;
- -i|--integration)
- INTEGRATION_ONLY=true
- shift
- ;;
- -e|--e2e)
- E2E_ONLY=true
- shift
- ;;
- -f|--fast)
- FAST_MODE=true
- shift
- ;;
- -c|--coverage)
- COVERAGE=true
- shift
- ;;
- --skip-build)
- SKIP_BUILD=true
- shift
- ;;
- --parallel)
- PARALLEL=true
- shift
- ;;
- *)
- echo "Opção desconhecida: $1"
- show_help
- exit 1
- ;;
- esac
-done
-
-# === Navegar para raiz do projeto ===
-cd "$PROJECT_ROOT" || { print_error "Falha ao acessar PROJECT_ROOT: $PROJECT_ROOT"; exit 1; }
-
-# === Preparação do Ambiente ===
-setup_test_environment() {
- print_header "Preparando Ambiente de Testes"
-
- # Criar diretórios de resultados
- print_verbose "Criando diretórios de resultados..."
- mkdir -p "$TEST_RESULTS_DIR"
- mkdir -p "$COVERAGE_DIR"
-
- # Limpar resultados antigos
- print_verbose "Limpando resultados antigos..."
-
- # Verificar e limpar diretório de resultados de teste
- if [ -n "$TEST_RESULTS_DIR" ] && [ -d "$TEST_RESULTS_DIR" ]; then
- find "$TEST_RESULTS_DIR" -maxdepth 1 -type f -name '*.trx' -delete 2>/dev/null || true
- fi
-
- # Verificar e limpar diretório de cobertura
- if [ -n "$COVERAGE_DIR" ] && [ -d "$COVERAGE_DIR" ]; then
- find "$COVERAGE_DIR" -mindepth 1 -maxdepth 1 -delete 2>/dev/null || true
- fi
-
- # Verificar Docker se necessário
- if [ "$INTEGRATION_ONLY" = true ] || [ "$E2E_ONLY" = true ] || { [ "$UNIT_ONLY" = false ] && [ "$INTEGRATION_ONLY" = false ] && [ "$E2E_ONLY" = false ]; }; then
- print_verbose "Verificando Docker para testes de integração..."
- if ! docker info &> /dev/null; then
- print_warning "Docker não está rodando. Testes de integração serão pulados."
- DOCKER_AVAILABLE=false
- else
- print_info "Docker disponível para testes de integração."
- DOCKER_AVAILABLE=true
- fi
- # Export for use in subshells/functions
- export DOCKER_AVAILABLE
- fi
-
- print_info "Ambiente de testes preparado!"
-}
-
-# === Aplicar Otimizações ===
-apply_optimizations() {
- if [ "$FAST_MODE" = true ]; then
- print_header "Aplicando Otimizações de Performance"
-
- print_info "Configurando variáveis de ambiente para otimização..."
-
- # Configurações Docker/TestContainers
- # Set DOCKER_HOST only on Windows platforms and only if not already defined
- if [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "win32" ]] || [[ "${OS:-}" == "Windows_NT" ]]; then
- if [[ -z "${DOCKER_HOST:-}" ]]; then
- export DOCKER_HOST="npipe://./pipe/docker_engine"
- print_verbose "Docker Host configurado para Windows"
- fi
- fi
-
- # TestContainers optimizations (apply on all platforms)
- export TESTCONTAINERS_RYUK_DISABLED=true
- export TESTCONTAINERS_CHECKS_DISABLE=true
- export TESTCONTAINERS_WAIT_STRATEGY_RETRIES=1
-
- # Configurações .NET para testes
- export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
- export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
- export DOTNET_CLI_TELEMETRY_OPTOUT=1
- export DOTNET_RUNNING_IN_CONTAINER=1
- export ASPNETCORE_ENVIRONMENT=Testing
- export COMPlus_EnableDiagnostics=0
- export COMPlus_TieredCompilation=0
- export DOTNET_TieredCompilation=0
- export DOTNET_ReadyToRun=0
- export DOTNET_TC_QuickJitForLoops=1
-
- # Configurações PostgreSQL para testes
- export POSTGRES_SHARED_PRELOAD_LIBRARIES=""
- export POSTGRES_LOGGING_COLLECTOR=off
- export POSTGRES_LOG_STATEMENT=none
- export POSTGRES_LOG_DURATION=off
- export POSTGRES_LOG_CHECKPOINTS=off
- export POSTGRES_CHECKPOINT_COMPLETION_TARGET=0.9
- export POSTGRES_WAL_BUFFERS=16MB
- export POSTGRES_SHARED_BUFFERS=256MB
- export POSTGRES_EFFECTIVE_CACHE_SIZE=1GB
- export POSTGRES_MAINTENANCE_WORK_MEM=64MB
- export POSTGRES_WORK_MEM=4MB
- export POSTGRES_FSYNC=off
- export POSTGRES_SYNCHRONOUS_COMMIT=off
- export POSTGRES_FULL_PAGE_WRITES=off
-
- print_info "Otimizações aplicadas! Potencial de melhoria significativa de performance (dependente do ambiente)."
- fi
-}
-
-# === Build da Solução ===
-build_solution() {
- if [ "$SKIP_BUILD" = true ]; then
- print_info "Pulando build..."
- return 0
- fi
-
- print_header "Compilando Solução para Testes"
-
- print_info "Restaurando dependências..."
- dotnet restore
-
- print_info "Compilando em modo Release..."
- if [ "$VERBOSE" = true ]; then
- if dotnet build --no-restore --configuration Release --verbosity normal; then
- print_info "Build concluído com sucesso!"
- else
- print_error "Falha no build. Verifique os erros acima."
- exit 1
- fi
- else
- if dotnet build --no-restore --configuration Release --verbosity minimal; then
- print_info "Build concluído com sucesso!"
- else
- print_error "Falha no build. Verifique os erros acima."
- exit 1
- fi
- fi
-}
-
-# === Testes Unitários ===
-run_unit_tests() {
- print_header "Executando Testes Unitários"
-
- local -a args=(--no-build --configuration Release \
- --filter 'Category!=Integration&Category!=E2E' \
- --logger "trx;LogFileName=unit-tests.trx" \
- --results-directory "$TEST_RESULTS_DIR")
-
- if [ "$VERBOSE" = true ]; then
- args+=(--logger "console;verbosity=normal")
- else
- args+=(--logger "console;verbosity=minimal")
- fi
-
- if [ "$COVERAGE" = true ]; then
- args+=(--collect:"XPlat Code Coverage")
- fi
-
- # Configure test parallelization via runsettings
- if [ "$PARALLEL" = true ]; then
- args+=(--settings "tests/parallel.runsettings")
- else
- args+=(--settings "tests/sequential.runsettings")
- fi
-
- print_info "Executando testes unitários..."
- if dotnet test "${args[@]}"; then
- print_info "Testes unitários concluídos com sucesso!"
- else
- print_error "Alguns testes unitários falharam."
- return 1
- fi
-}
-
-# === Validação de Namespaces ===
-validate_namespace_reorganization() {
- print_header "Validando Reorganização de Namespaces"
-
- print_info "Verificando conformidade com a reorganização de namespaces..."
-
- # Verificar se não há referências ao namespace antigo
- if grep -R -q --include='*.cs' -E '^[[:space:]]*using[[:space:]]+MeAjudaAi\.Shared\.Common;' src/ 2>/dev/null; then
- print_error "❌ Encontradas referências ao namespace antigo MeAjudaAi.Shared.Common"
- print_error " Use os novos namespaces específicos:"
- print_error " - MeAjudaAi.Shared.Functional (Result, Error, Unit)"
- print_error " - MeAjudaAi.Shared.Domain (BaseEntity, AggregateRoot, ValueObject)"
- print_error " - MeAjudaAi.Shared.Contracts (Request, Response, PagedRequest, PagedResponse)"
- print_error " - MeAjudaAi.Shared.Mediator (IRequest, IPipelineBehavior)"
- print_error " - MeAjudaAi.Shared.Security (UserRoles)"
- return 1
- fi
-
- # Verificar se os novos namespaces estão sendo usados
- local functional_count
- functional_count=$({ grep -R -l --include='*.cs' 'MeAjudaAi\.Shared\.Functional' src/ 2>/dev/null || true; } | wc -l)
- local domain_count
- domain_count=$({ grep -R -l --include='*.cs' 'MeAjudaAi\.Shared\.Domain' src/ 2>/dev/null || true; } | wc -l)
- local contracts_count
- contracts_count=$({ grep -R -l --include='*.cs' 'MeAjudaAi\.Shared\.Contracts' src/ 2>/dev/null || true; } | wc -l)
-
- print_info "Estatísticas de uso dos novos namespaces:"
- print_info "- Functional: $functional_count arquivos"
- print_info "- Domain: $domain_count arquivos"
- print_info "- Contracts: $contracts_count arquivos"
-
- print_info "✅ Reorganização de namespaces validada com sucesso!"
- return 0
-}
-
-# === Testes Específicos por Projeto ===
-run_specific_project_tests() {
- print_header "Executando Testes por Projeto"
-
- local failed_projects=0
-
- # Build common args array
- local -a common_args=(
- --no-build
- --configuration "$CONFIG"
- )
-
- # Add coverage collection if enabled
- if [ "$COVERAGE" = true ]; then
- common_args+=(--collect:"XPlat Code Coverage")
- fi
-
- # Configure test parallelization via runsettings
- if [ "$PARALLEL" = true ]; then
- common_args+=(--settings "tests/parallel.runsettings")
- else
- common_args+=(--settings "tests/sequential.runsettings")
- fi
-
- # Set verbosity
- local verbosity_level="minimal"
- if [ "$VERBOSE" = true ]; then
- verbosity_level="normal"
- fi
-
- # Testes do Shared
- print_info "Executando testes MeAjudaAi.Shared.Tests..."
- if dotnet test tests/MeAjudaAi.Shared.Tests/MeAjudaAi.Shared.Tests.csproj \
- "${common_args[@]}" \
- --logger "console;verbosity=$verbosity_level" \
- --logger "trx;LogFileName=shared-tests.trx" \
- --results-directory "$TEST_RESULTS_DIR"; then
- print_info "✅ MeAjudaAi.Shared.Tests passou"
- else
- print_error "❌ MeAjudaAi.Shared.Tests falhou"
- failed_projects=$((failed_projects + 1))
- fi
-
- # Testes de Arquitetura
- print_info "Executando testes MeAjudaAi.Architecture.Tests..."
- if dotnet test tests/MeAjudaAi.Architecture.Tests/MeAjudaAi.Architecture.Tests.csproj \
- "${common_args[@]}" \
- --logger "console;verbosity=$verbosity_level" \
- --logger "trx;LogFileName=architecture-tests.trx" \
- --results-directory "$TEST_RESULTS_DIR"; then
- print_info "✅ MeAjudaAi.Architecture.Tests passou"
- else
- print_error "❌ MeAjudaAi.Architecture.Tests falhou"
- failed_projects=$((failed_projects + 1))
- fi
-
- # Testes de Integração (conditional on Docker availability)
- if [ "$DOCKER_AVAILABLE" = true ]; then
- print_info "Executando testes MeAjudaAi.Integration.Tests..."
- if ASPNETCORE_ENVIRONMENT=Testing dotnet test tests/MeAjudaAi.Integration.Tests/MeAjudaAi.Integration.Tests.csproj \
- "${common_args[@]}" \
- --logger "console;verbosity=$verbosity_level" \
- --logger "trx;LogFileName=integration-tests.trx" \
- --results-directory "$TEST_RESULTS_DIR"; then
- print_info "✅ MeAjudaAi.Integration.Tests passou"
- else
- print_error "❌ MeAjudaAi.Integration.Tests falhou"
- failed_projects=$((failed_projects + 1))
- fi
- else
- print_warning "⏭️ Pulando MeAjudaAi.Integration.Tests (Docker não disponível)"
- fi
-
- # Testes E2E (conditional on Docker availability)
- if [ "$DOCKER_AVAILABLE" = true ]; then
- print_info "Executando testes MeAjudaAi.E2E.Tests..."
- if ASPNETCORE_ENVIRONMENT=Testing dotnet test tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj \
- "${common_args[@]}" \
- --logger "console;verbosity=$verbosity_level" \
- --logger "trx;LogFileName=e2e-tests.trx" \
- --results-directory "$TEST_RESULTS_DIR"; then
- print_info "✅ MeAjudaAi.E2E.Tests passou"
- else
- print_error "❌ MeAjudaAi.E2E.Tests falhou"
- failed_projects=$((failed_projects + 1))
- fi
- else
- print_warning "⏭️ Pulando MeAjudaAi.E2E.Tests (Docker não disponível)"
- fi
-
- if [ "$failed_projects" -eq 0 ]; then
- print_info "✅ Todos os projetos de teste passaram!"
- return 0
- else
- print_error "❌ $failed_projects projeto(s) de teste falharam"
- return 1
- fi
-}
-run_integration_tests() {
- print_header "Executando Testes de Integração"
-
- # Verificar se Docker está disponível
- if ! docker info &> /dev/null; then
- print_warning "Docker não disponível. Pulando testes de integração."
- return 0
- fi
-
- local -a args=(--no-build --configuration Release \
- --filter 'Category=Integration' \
- --logger "trx;LogFileName=integration-tests.trx" \
- --results-directory "$TEST_RESULTS_DIR")
-
- if [ "$VERBOSE" = true ]; then
- args+=(--logger "console;verbosity=normal")
- else
- args+=(--logger "console;verbosity=minimal")
- fi
-
- if [ "$COVERAGE" = true ]; then
- args+=(--collect:"XPlat Code Coverage")
- fi
-
- # Configure test parallelization via runsettings
- if [ "$PARALLEL" = true ]; then
- args+=(--settings "tests/parallel.runsettings")
- else
- args+=(--settings "tests/sequential.runsettings")
- fi
-
- print_info "Executando testes de integração..."
- if dotnet test "${args[@]}"; then
- print_info "Testes de integração concluídos com sucesso!"
- else
- print_error "Alguns testes de integração falharam."
- return 1
- fi
-}
-
-# === Testes E2E ===
-run_e2e_tests() {
- print_header "Executando Testes End-to-End"
-
- # Check Docker availability for E2E tests
- print_verbose "Verificando disponibilidade do Docker para testes E2E..."
- if ! command -v docker &> /dev/null; then
- print_warning "Docker não está instalado. Pulando testes E2E."
- return 0
- fi
-
- if ! docker info &> /dev/null; then
- print_warning "Docker não está rodando ou não está acessível. Pulando testes E2E."
- return 0
- fi
-
- print_info "Docker disponível. Prosseguindo com testes E2E..."
-
- local -a args=(--no-build --configuration Release \
- --filter 'Category=E2E' \
- --logger "trx;LogFileName=e2e-tests.trx" \
- --results-directory "$TEST_RESULTS_DIR")
-
- if [ "$VERBOSE" = true ]; then
- args+=(--logger "console;verbosity=normal")
- else
- args+=(--logger "console;verbosity=minimal")
- fi
-
- if [ "$COVERAGE" = true ]; then
- args+=(--collect:"XPlat Code Coverage")
- fi
-
- # Configure test parallelization via runsettings
- if [ "$PARALLEL" = true ]; then
- args+=(--settings "tests/parallel.runsettings")
- else
- args+=(--settings "tests/sequential.runsettings")
- fi
-
- print_info "Executando testes E2E..."
- if dotnet test "${args[@]}"; then
- print_info "Testes E2E concluídos com sucesso!"
- else
- print_error "Alguns testes E2E falharam."
- return 1
- fi
-}
-
-# === Gerar Relatório de Cobertura ===
-generate_coverage_report() {
- if [ "$COVERAGE" = false ]; then
- return 0
- fi
-
- print_header "Gerando Relatório de Cobertura"
-
- # Verificar se reportgenerator está instalado
- if ! command -v reportgenerator &> /dev/null; then
- print_warning "reportgenerator não encontrado. Instalando..."
- dotnet tool install --global dotnet-reportgenerator-globaltool
-
- # Adicionar diretório de ferramentas do dotnet ao PATH se não estiver presente
- if [[ ":$PATH:" != *":$HOME/.dotnet/tools:"* ]]; then
- export PATH="$PATH:$HOME/.dotnet/tools"
- print_verbose "Adicionado $HOME/.dotnet/tools ao PATH"
- fi
- fi
-
- print_info "Processando arquivos de cobertura..."
- if reportgenerator \
- -reports:"$TEST_RESULTS_DIR/**/coverage.cobertura.xml" \
- -targetdir:"$COVERAGE_DIR" \
- -reporttypes:"Html;Cobertura;TextSummary" \
- -verbosity:Warning; then
- print_info "Relatório de cobertura gerado em: $COVERAGE_DIR"
- print_info "Abra o arquivo index.html no navegador para visualizar."
- else
- print_warning "Erro ao gerar relatório de cobertura."
- fi
-}
-
-# === Relatório de Resultados ===
-show_results() {
- print_header "Resultados dos Testes"
-
- # Contar arquivos de resultado
- local trx_files
- trx_files=$(find "$TEST_RESULTS_DIR" -name "*.trx" 2>/dev/null | wc -l)
-
- if [ "$trx_files" -gt 0 ]; then
- print_info "Arquivos de resultado gerados: $trx_files"
- print_info "Localização: $TEST_RESULTS_DIR"
- fi
-
- if [ "$COVERAGE" = true ] && [ -f "$COVERAGE_DIR/index.html" ]; then
- print_info "Relatório de cobertura disponível em: $COVERAGE_DIR/index.html"
- fi
-
- if [ "$FAST_MODE" = true ]; then
- print_info "Modo otimizado usado - performance melhorada!"
- fi
-}
-
-# === Execução Principal ===
-main() {
- local start_time
- local failed_tests=0
- start_time=$(date +%s)
-
- setup_test_environment
- apply_optimizations
- build_solution
-
- # Validar reorganização de namespaces primeiro
- validate_namespace_reorganization || failed_tests=$((failed_tests + 1))
-
- # Executar testes baseado nas opções
- if [ "$UNIT_ONLY" = true ]; then
- run_unit_tests || failed_tests=$((failed_tests + 1))
- elif [ "$INTEGRATION_ONLY" = true ]; then
- run_integration_tests || failed_tests=$((failed_tests + 1))
- elif [ "$E2E_ONLY" = true ]; then
- run_e2e_tests || failed_tests=$((failed_tests + 1))
- else
- # Executar todos os tipos de teste com projetos específicos
- run_specific_project_tests || failed_tests=$((failed_tests + 1))
- fi
-
- generate_coverage_report
- show_results
-
- local end_time
- local duration
- end_time=$(date +%s)
- duration=$((end_time - start_time))
-
- print_header "Resumo da Execução"
- print_info "Tempo total: ${duration}s"
-
- if [ "$failed_tests" -eq 0 ]; then
- print_info "Todos os testes foram executados com sucesso! 🎉"
- exit 0
- else
- print_error "Alguns conjuntos de testes falharam. Total: $failed_tests"
- exit 1
- fi
-}
-
-# === Execução ===
-main "$@"
\ No newline at end of file
diff --git a/scripts/track-coverage-progress.ps1 b/scripts/track-coverage-progress.ps1
deleted file mode 100644
index 48072433a..000000000
--- a/scripts/track-coverage-progress.ps1
+++ /dev/null
@@ -1,242 +0,0 @@
-#!/usr/bin/env pwsh
-<#
-.SYNOPSIS
- Track code coverage progress toward 70% target
-
-.DESCRIPTION
- Runs tests with coverage, generates report, and shows progress metrics
-
-.EXAMPLE
- .\track-coverage-progress.ps1
- .\track-coverage-progress.ps1 -SkipTests (use existing coverage data)
-#>
-
-param(
- [switch]$SkipTests
-)
-
-$ErrorActionPreference = "Stop"
-
-Write-Host "🎯 Coverage Progress Tracker" -ForegroundColor Cyan
-Write-Host "Target: 70% | Current: ?" -ForegroundColor Gray
-Write-Host ""
-
-# Define target
-$TARGET_COVERAGE = 70.0
-
-if (-not $SkipTests) {
- Write-Host "▶️ Running tests with coverage collection..." -ForegroundColor Yellow
-
- # Clean previous results
- if (Test-Path "TestResults") {
- Remove-Item -Recurse -Force "TestResults"
- }
-
- # Run tests with coverage
- dotnet test --collect:"XPlat Code Coverage" --results-directory TestResults `
- -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover | Out-Null
-
- if ($LASTEXITCODE -ne 0) {
- Write-Host "⚠️ Some tests failed, but continuing with coverage analysis..." -ForegroundColor Yellow
- }
-}
-
-Write-Host ""
-Write-Host "📊 Generating coverage report..." -ForegroundColor Yellow
-
-# Generate report
-reportgenerator `
- -reports:"TestResults/**/coverage.opencover.xml" `
- -targetdir:"CoverageReport" `
- -reporttypes:"Html;JsonSummary;Cobertura" | Out-Null
-
-# Read summary
-$summary = Get-Content "CoverageReport\Summary.json" | ConvertFrom-Json
-
-# Extract metrics
-$lineCoverage = $summary.summary.linecoverage
-$branchCoverage = $summary.summary.branchcoverage
-$methodCoverage = $summary.summary.methodcoverage
-$coveredLines = $summary.summary.coveredlines
-$coverableLines = $summary.summary.coverablelines
-$uncoveredLines = $summary.summary.uncoveredlines
-
-# Calculate progress
-$progressPercentage = ($lineCoverage / $TARGET_COVERAGE) * 100
-$remainingLines = [Math]::Ceiling($coverableLines * ($TARGET_COVERAGE / 100) - $coveredLines)
-$remainingPercentage = $TARGET_COVERAGE - $lineCoverage
-
-# Display results
-Write-Host ""
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "📈 COVERAGE SUMMARY" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-Write-Host " Line Coverage: " -NoNewline -ForegroundColor White
-if ($lineCoverage -ge $TARGET_COVERAGE) {
- Write-Host "$lineCoverage% ✅" -ForegroundColor Green
-} elseif ($lineCoverage -ge 50) {
- Write-Host "$lineCoverage% 🟡" -ForegroundColor Yellow
-} else {
- Write-Host "$lineCoverage% 🔴" -ForegroundColor Red
-}
-
-Write-Host " Branch Coverage: " -NoNewline -ForegroundColor White
-Write-Host "$branchCoverage%" -ForegroundColor Gray
-
-Write-Host " Method Coverage: " -NoNewline -ForegroundColor White
-Write-Host "$methodCoverage%" -ForegroundColor Gray
-
-Write-Host ""
-Write-Host " Covered Lines: " -NoNewline -ForegroundColor White
-Write-Host "$coveredLines / $coverableLines" -ForegroundColor Gray
-
-Write-Host " Uncovered Lines: " -NoNewline -ForegroundColor White
-Write-Host "$uncoveredLines" -ForegroundColor Red
-
-Write-Host ""
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "🎯 PROGRESS TO TARGET (70%)" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-# Progress bar
-$barLength = 40
-$filledLength = [Math]::Floor($barLength * ($lineCoverage / $TARGET_COVERAGE))
-$emptyLength = $barLength - $filledLength
-$progressBar = "█" * $filledLength + "░" * $emptyLength
-
-Write-Host " [$progressBar] " -NoNewline
-Write-Host "$([Math]::Round($progressPercentage, 1))%" -ForegroundColor Cyan
-
-Write-Host ""
-Write-Host " Current: " -NoNewline -ForegroundColor White
-Write-Host "$lineCoverage%" -ForegroundColor $(if ($lineCoverage -ge 50) { "Yellow" } else { "Red" })
-
-Write-Host " Target: " -NoNewline -ForegroundColor White
-Write-Host "$TARGET_COVERAGE%" -ForegroundColor Green
-
-Write-Host " Remaining: " -NoNewline -ForegroundColor White
-Write-Host "+$([Math]::Round($remainingPercentage, 1))pp ($remainingLines lines)" -ForegroundColor Magenta
-
-Write-Host ""
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "📋 TOP 10 MODULES TO IMPROVE" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-# Get bottom 10 assemblies by coverage (excluding generated code)
-$assemblies = $summary.coverage.assemblies |
- Where-Object { $_.name -notmatch "Generated|CompilerServices" } |
- Sort-Object coverage |
- Select-Object -First 10
-
-foreach ($assembly in $assemblies) {
- $name = $assembly.name -replace "MeAjudaAi\.", ""
- $coverage = $assembly.coverage
- $uncovered = $assembly.coverablelines - $assembly.coveredlines
-
- # Shorten name if too long
- if ($name.Length -gt 40) {
- $name = $name.Substring(0, 37) + "..."
- }
-
- # Color based on coverage
- $color = if ($coverage -ge 70) { "Green" }
- elseif ($coverage -ge 50) { "Yellow" }
- elseif ($coverage -ge 30) { "DarkYellow" }
- else { "Red" }
-
- # Format with padding
- $namePadded = $name.PadRight(45)
- $coveragePadded = "$coverage%".PadLeft(6)
- $uncoveredPadded = "+$uncovered lines".PadLeft(12)
-
- Write-Host " $namePadded " -NoNewline -ForegroundColor Gray
- Write-Host "$coveragePadded " -NoNewline -ForegroundColor $color
- Write-Host "$uncoveredPadded" -ForegroundColor DarkGray
-}
-
-Write-Host ""
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "💡 QUICK WINS (High Impact, Low Effort)" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-# Analyze classes with 0% coverage and <100 lines
-$quickWins = @()
-foreach ($assembly in $summary.coverage.assemblies) {
- if ($assembly.name -match "Generated|CompilerServices") { continue }
-
- foreach ($class in $assembly.classesinassembly) {
- if ($class.coverage -eq 0 -and $class.coverablelines -gt 10 -and $class.coverablelines -lt 150) {
- $quickWins += [PSCustomObject]@{
- Assembly = $assembly.name -replace "MeAjudaAi\.", ""
- Class = $class.name
- Lines = $class.coverablelines
- Impact = $class.coverablelines / $coverableLines * 100
- }
- }
- }
-}
-
-$topQuickWins = $quickWins | Sort-Object -Descending Lines | Select-Object -First 5
-
-foreach ($win in $topQuickWins) {
- $className = $win.Class
- if ($className.Length -gt 50) {
- $className = $className.Substring(0, 47) + "..."
- }
-
- Write-Host " • " -NoNewline -ForegroundColor Yellow
- Write-Host "$className" -NoNewline -ForegroundColor White
- Write-Host " ($($win.Lines) lines, +$([Math]::Round($win.Impact, 2))pp)" -ForegroundColor DarkGray
-}
-
-Write-Host ""
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host "🚀 NEXT STEPS" -ForegroundColor Cyan
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-if ($lineCoverage -lt 20) {
- Write-Host " 1. Focus on Infrastructure layer repositories" -ForegroundColor Yellow
- Write-Host " 2. Add basic CRUD tests for uncovered repos" -ForegroundColor Yellow
- Write-Host " 3. See: docs/testing/coverage-improvement-plan.md" -ForegroundColor Gray
-} elseif ($lineCoverage -lt 40) {
- Write-Host " 1. Complete repository test coverage" -ForegroundColor Yellow
- Write-Host " 2. Add domain event handler tests" -ForegroundColor Yellow
- Write-Host " 3. Review 'Quick Wins' list above" -ForegroundColor Gray
-} elseif ($lineCoverage -lt 60) {
- Write-Host " 1. Add application handler tests" -ForegroundColor Yellow
- Write-Host " 2. Improve domain layer coverage" -ForegroundColor Yellow
- Write-Host " 3. Start API E2E tests" -ForegroundColor Gray
-} else {
- Write-Host " 1. Add edge case tests" -ForegroundColor Yellow
- Write-Host " 2. Complete E2E test coverage" -ForegroundColor Yellow
- Write-Host " 3. Final push to 70%!" -ForegroundColor Green
-}
-
-Write-Host ""
-Write-Host " 📖 Full plan: " -NoNewline -ForegroundColor White
-Write-Host "docs/testing/coverage-improvement-plan.md" -ForegroundColor Cyan
-
-Write-Host " 📊 HTML Report: " -NoNewline -ForegroundColor White
-Write-Host "CoverageReport/index.html" -ForegroundColor Cyan
-
-Write-Host " 🔍 Gap Script: " -NoNewline -ForegroundColor White
-Write-Host "scripts/find-coverage-gaps.ps1" -ForegroundColor Cyan
-
-Write-Host ""
-Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan
-Write-Host ""
-
-# Exit with error if below target
-if ($lineCoverage -lt $TARGET_COVERAGE) {
- Write-Host "⚠️ Coverage below target ($lineCoverage% < $TARGET_COVERAGE%)" -ForegroundColor Yellow
- exit 1
-} else {
- Write-Host "✅ Coverage target reached! ($lineCoverage% >= $TARGET_COVERAGE%)" -ForegroundColor Green
- exit 0
-}
diff --git a/scripts/utils.sh b/scripts/utils.sh
deleted file mode 100644
index 59ab5ae62..000000000
--- a/scripts/utils.sh
+++ /dev/null
@@ -1,586 +0,0 @@
-#!/bin/bash
-
-# =============================================================================
-# MeAjudaAi Shared Utilities - Funções Comuns para Scripts
-# =============================================================================
-# Biblioteca de funções compartilhadas entre os scripts do projeto.
-# Inclui logging, validações, configurações e helpers comuns.
-#
-# Uso:
-# source ./scripts/utils.sh
-# ou
-# . ./scripts/utils.sh
-#
-# Funções disponíveis:
-# - Logging: print_*, log_*
-# - Validações: check_*, validate_*
-# - Sistema: detect_*, get_*
-# - Configuração: load_*, save_*
-# - Docker: docker_*, container_*
-# - .NET: dotnet_*, nuget_*
-# =============================================================================
-
-# === Verificar se já foi carregado ===
-if [ "${MEAJUDAAI_UTILS_LOADED:-}" = "true" ]; then
- return 0 2>/dev/null || true
- exit 0
-fi
-
-# === Configurações Globais ===
-MEAJUDAAI_UTILS_LOADED=true
-UTILS_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-UTILS_PROJECT_ROOT="$(cd "$UTILS_SCRIPT_DIR/.." && pwd)"
-
-# === Cores para output ===
-RED='\033[0;31m'
-GREEN='\033[0;32m'
-YELLOW='\033[1;33m'
-BLUE='\033[0;34m'
-CYAN='\033[0;36m'
-MAGENTA='\033[0;35m'
-WHITE='\033[1;37m'
-NC='\033[0m' # No Color
-
-# === Níveis de Log ===
-LOG_LEVEL_ERROR=1
-LOG_LEVEL_WARN=2
-LOG_LEVEL_INFO=3
-LOG_LEVEL_DEBUG=4
-LOG_LEVEL_VERBOSE=5
-
-# Nível padrão (pode ser sobrescrito por variável de ambiente)
-CURRENT_LOG_LEVEL=${MEAJUDAAI_LOG_LEVEL:-$LOG_LEVEL_INFO}
-
-# ============================================================================
-# FUNÇÕES DE LOGGING
-# ============================================================================
-
-# === Logging com timestamp ===
-log_with_timestamp() {
- local level="$1"
- local message="$2"
- local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
- echo -e "${timestamp} [${level}] ${message}"
-}
-
-# === Print functions (sem timestamp) ===
-print_header() {
- echo -e "${BLUE}===================================================================${NC}"
- echo -e "${BLUE} $1${NC}"
- echo -e "${BLUE}===================================================================${NC}"
-}
-
-print_subheader() {
- echo -e "${CYAN}--- $1 ---${NC}"
-}
-
-print_info() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_INFO" ]; then
- echo -e "${GREEN}✅ $1${NC}"
- fi
-}
-
-print_success() {
- echo -e "${GREEN}🎉 $1${NC}"
-}
-
-print_warning() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_WARN" ]; then
- echo -e "${YELLOW}⚠️ $1${NC}"
- fi
-}
-
-print_error() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_ERROR" ]; then
- echo -e "${RED}❌ $1${NC}" >&2
- fi
-}
-
-print_debug() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_DEBUG" ]; then
- echo -e "${CYAN}🔍 $1${NC}"
- fi
-}
-
-print_verbose() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_VERBOSE" ]; then
- echo -e "${MAGENTA}📝 $1${NC}"
- fi
-}
-
-print_step() {
- echo -e "${BLUE}🔧 $1${NC}"
-}
-
-print_progress() {
- echo -e "${WHITE}⏳ $1${NC}"
-}
-
-# === Log functions (com timestamp) ===
-log_info() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_INFO" ]; then
- log_with_timestamp "INFO" "${GREEN}$1${NC}"
- fi
-}
-
-log_warning() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_WARN" ]; then
- log_with_timestamp "WARN" "${YELLOW}$1${NC}"
- fi
-}
-
-log_error() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_ERROR" ]; then
- log_with_timestamp "ERROR" "${RED}$1${NC}" >&2
- fi
-}
-
-log_debug() {
- if [ "$CURRENT_LOG_LEVEL" -ge "$LOG_LEVEL_DEBUG" ]; then
- log_with_timestamp "DEBUG" "${CYAN}$1${NC}"
- fi
-}
-
-# ============================================================================
-# FUNÇÕES DE VALIDAÇÃO E VERIFICAÇÃO
-# ============================================================================
-
-# === Verificar se comando existe ===
-command_exists() {
- command -v "$1" &> /dev/null
-}
-
-# === Verificar se arquivo existe ===
-file_exists() {
- [ -f "$1" ]
-}
-
-# === Verificar se diretório existe ===
-dir_exists() {
- [ -d "$1" ]
-}
-
-# === Verificar dependências essenciais ===
-check_essential_dependencies() {
- local missing_deps=()
-
- if ! command_exists dotnet; then
- missing_deps+=(".NET SDK")
- fi
-
- if ! command_exists git; then
- missing_deps+=("Git")
- fi
-
- if [ ${#missing_deps[@]} -gt 0 ]; then
- print_error "Dependências essenciais não encontradas:"
- for dep in "${missing_deps[@]}"; do
- print_error " - $dep"
- done
- return 1
- fi
-
- return 0
-}
-
-# === Verificar dependências opcionais ===
-check_optional_dependencies() {
- local warnings=()
-
- if ! command_exists docker; then
- warnings+=("Docker não encontrado - testes de integração não funcionarão")
- fi
-
- if ! command_exists az; then
- warnings+=("Azure CLI não encontrado - deploy não funcionará")
- fi
-
- if ! command_exists code; then
- warnings+=("VS Code não encontrado")
- fi
-
- if [ ${#warnings[@]} -gt 0 ]; then
- print_warning "Dependências opcionais não encontradas:"
- for warning in "${warnings[@]}"; do
- print_warning " - $warning"
- done
- fi
-}
-
-# === Validar argumentos ===
-validate_argument() {
- local arg_name="$1"
- local arg_value="$2"
- local required="${3:-true}"
-
- if [ "$required" = "true" ] && [ -z "$arg_value" ]; then
- print_error "Argumento obrigatório não fornecido: $arg_name"
- return 1
- fi
-
- return 0
-}
-
-# === Validar ambiente ===
-validate_environment() {
- local env="$1"
- case $env in
- dev|development|prod|production)
- return 0
- ;;
- *)
- print_error "Ambiente inválido: $env"
- print_error "Ambientes válidos: dev, development, prod, production"
- return 1
- ;;
- esac
-}
-
-# ============================================================================
-# FUNÇÕES DE SISTEMA
-# ============================================================================
-
-# === Detectar sistema operacional ===
-detect_os() {
- local os_type=""
-
- if [[ "$OSTYPE" == "linux-gnu"* ]]; then
- os_type="linux"
- if command_exists lsb_release; then
- DISTRO=$(lsb_release -si 2>/dev/null)
- else
- DISTRO="Unknown"
- fi
- elif [[ "$OSTYPE" == "darwin"* ]]; then
- os_type="macos"
- DISTRO="macOS"
- elif [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then
- os_type="windows"
- DISTRO="Windows"
- else
- os_type="unknown"
- DISTRO="Unknown"
- fi
-
- export OS_TYPE="$os_type"
- export OS_DISTRO="$DISTRO"
-
- print_debug "Sistema detectado: $os_type ($DISTRO)"
- return 0
-}
-
-# === Obter informações do sistema ===
-get_system_info() {
- detect_os
-
- # CPU info
- if command_exists nproc; then
- CPU_CORES=$(nproc)
- elif command_exists sysctl; then
- CPU_CORES=$(sysctl -n hw.ncpu)
- else
- CPU_CORES=1
- fi
-
- # Memory info (em GB)
- if [ "$OS_TYPE" = "linux" ]; then
- MEMORY_GB=$(free -g | awk 'NR==2{print $2}')
- elif [ "$OS_TYPE" = "macos" ]; then
- MEMORY_BYTES=$(sysctl -n hw.memsize)
- MEMORY_GB=$((MEMORY_BYTES / 1024 / 1024 / 1024))
- else
- MEMORY_GB=8 # Default fallback
- fi
-
- export CPU_CORES
- export MEMORY_GB
-
- print_debug "CPU Cores: $CPU_CORES, Memory: ${MEMORY_GB}GB"
-}
-
-# === Obter caminho absoluto ===
-get_absolute_path() {
- local path="$1"
-
- if [ -d "$path" ]; then
- (cd "$path" && pwd)
- elif [ -f "$path" ]; then
- echo "$(cd "$(dirname "$path")" && pwd)/$(basename "$path")"
- else
- echo "$path"
- fi
-}
-
-# ============================================================================
-# FUNÇÕES DE CONFIGURAÇÃO
-# ============================================================================
-
-# === Carregar configuração do projeto ===
-load_project_config() {
- local config_file="$UTILS_PROJECT_ROOT/.meajudaai.config"
-
- if [ -f "$config_file" ]; then
- source "$config_file"
- print_debug "Configuração carregada de: $config_file"
- else
- print_debug "Arquivo de configuração não encontrado: $config_file"
- fi
-}
-
-# === Salvar configuração ===
-save_project_config() {
- local config_file="$UTILS_PROJECT_ROOT/.meajudaai.config"
- local key="$1"
- local value="$2"
-
- # Criar arquivo se não existir
- touch "$config_file"
-
- # Remover linha existente e adicionar nova
- grep -v "^$key=" "$config_file" > "${config_file}.tmp" 2>/dev/null || true
- echo "$key=$value" >> "${config_file}.tmp"
- mv "${config_file}.tmp" "$config_file"
-
- print_debug "Configuração salva: $key=$value"
-}
-
-# === Obter configuração ===
-get_config() {
- local key="$1"
- local default="$2"
- local config_file="$UTILS_PROJECT_ROOT/.meajudaai.config"
-
- if [ -f "$config_file" ]; then
- local value=$(grep "^$key=" "$config_file" | cut -d'=' -f2-)
- echo "${value:-$default}"
- else
- echo "$default"
- fi
-}
-
-# ============================================================================
-# FUNÇÕES DOCKER
-# ============================================================================
-
-# === Verificar se Docker está rodando ===
-docker_is_running() {
- docker info &> /dev/null
-}
-
-# === Obter containers rodando ===
-docker_list_containers() {
- local filter="$1"
-
- if [ -n "$filter" ]; then
- docker ps --filter "$filter" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
- else
- docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
- fi
-}
-
-# === Parar containers por pattern ===
-docker_stop_containers() {
- local pattern="$1"
-
- if [ -z "$pattern" ]; then
- print_error "Pattern é obrigatório para docker_stop_containers"
- return 1
- fi
-
- local containers=$(docker ps --filter "name=$pattern" --format "{{.Names}}")
-
- if [ -n "$containers" ]; then
- print_info "Parando containers: $containers"
- echo "$containers" | xargs docker stop
- else
- print_info "Nenhum container encontrado com pattern: $pattern"
- fi
-}
-
-# === Limpar containers e volumes ===
-docker_cleanup() {
- print_step "Limpando containers e volumes Docker..."
-
- # Parar containers
- docker stop $(docker ps -q) 2>/dev/null || true
-
- # Remover containers
- docker rm $(docker ps -aq) 2>/dev/null || true
-
- # Remover volumes não utilizados
- docker volume prune -f 2>/dev/null || true
-
- print_info "Limpeza Docker concluída"
-}
-
-# ============================================================================
-# FUNÇÕES .NET
-# ============================================================================
-
-# === Verificar versão do .NET ===
-dotnet_get_version() {
- if command_exists dotnet; then
- dotnet --version
- else
- echo "not-installed"
- fi
-}
-
-# === Verificar se projeto é válido ===
-dotnet_is_valid_project() {
- local project_path="$1"
-
- if [ -f "$project_path" ] && [[ "$project_path" == *.csproj ]]; then
- return 0
- elif [ -d "$project_path" ] && find "$project_path" -name "*.csproj" -maxdepth 1 | grep -q .; then
- return 0
- else
- return 1
- fi
-}
-
-# === Build projeto com configuração ===
-dotnet_build_project() {
- local project="$1"
- local configuration="${2:-Release}"
- local verbosity="${3:-minimal}"
-
- print_step "Building projeto: $project"
-
- dotnet build "$project" \
- --configuration "$configuration" \
- --verbosity "$verbosity" \
- --no-restore
-}
-
-# === Executar testes com filtros ===
-dotnet_run_tests() {
- local project="$1"
- local filter="$2"
- local configuration="${3:-Release}"
-
- local test_args="--no-build --configuration $configuration"
-
- if [ -n "$filter" ]; then
- test_args="$test_args --filter \"$filter\""
- fi
-
- print_step "Executando testes: $project"
- eval "dotnet test \"$project\" $test_args"
-}
-
-# ============================================================================
-# FUNÇÕES DE REDE E PORTAS
-# ============================================================================
-
-# === Verificar se porta está em uso ===
-port_is_in_use() {
- local port="$1"
-
- if command_exists netstat; then
- netstat -an | grep ":$port " > /dev/null
- elif command_exists ss; then
- ss -an | grep ":$port " > /dev/null
- else
- return 1
- fi
-}
-
-# === Encontrar porta livre ===
-find_free_port() {
- local start_port="${1:-3000}"
- local max_port="${2:-65535}"
-
- for port in $(seq $start_port $max_port); do
- if ! port_is_in_use "$port"; then
- echo "$port"
- return 0
- fi
- done
-
- return 1
-}
-
-# ============================================================================
-# FUNÇÕES DE TEMPO E PERFORMANCE
-# ============================================================================
-
-# === Medir tempo de execução ===
-time_start() {
- TIMER_START=$(date +%s)
-}
-
-time_end() {
- local start_time="${TIMER_START:-$(date +%s)}"
- local end_time=$(date +%s)
- local duration=$((end_time - start_time))
-
- echo "$duration"
-}
-
-# === Formatar duração ===
-format_duration() {
- local seconds="$1"
-
- if [ "$seconds" -lt 60 ]; then
- echo "${seconds}s"
- elif [ "$seconds" -lt 3600 ]; then
- local minutes=$((seconds / 60))
- local remaining_seconds=$((seconds % 60))
- echo "${minutes}m ${remaining_seconds}s"
- else
- local hours=$((seconds / 3600))
- local remaining_minutes=$(((seconds % 3600) / 60))
- echo "${hours}h ${remaining_minutes}m"
- fi
-}
-
-# ============================================================================
-# FUNÇÕES DE CLEANUP E FINALIZAÇÃO
-# ============================================================================
-
-# === Cleanup automático ===
-cleanup_on_exit() {
- local cleanup_function="$1"
-
- trap "$cleanup_function" EXIT INT TERM
-}
-
-# === Remover arquivos temporários ===
-cleanup_temp_files() {
- local temp_pattern="${1:-/tmp/meajudaai_*}"
-
- print_debug "Removendo arquivos temporários: $temp_pattern"
- rm -f $temp_pattern 2>/dev/null || true
-}
-
-# ============================================================================
-# INICIALIZAÇÃO
-# ============================================================================
-
-# === Inicializar utils ===
-utils_init() {
- # Detectar sistema
- detect_os
-
- # Carregar configuração do projeto
- load_project_config
-
- print_debug "MeAjudaAi Utils inicializado"
-}
-
-# === Auto-inicialização (se não foi explicitamente desabilitada) ===
-if [ "${MEAJUDAAI_UTILS_AUTO_INIT:-true}" = "true" ]; then
- utils_init
-fi
-
-# === Exportar funções principais ===
-export -f print_header print_info print_success print_warning print_error print_debug print_verbose print_step
-export -f command_exists file_exists dir_exists check_essential_dependencies
-export -f detect_os get_system_info
-export -f docker_is_running docker_cleanup
-export -f dotnet_get_version dotnet_build_project
-export -f time_start time_end format_duration
-
-# === Marcar como carregado ===
-print_debug "MeAjudaAi Utilities carregado com sucesso! 🛠️"
\ No newline at end of file
diff --git a/src/Aspire/MeAjudaAi.AppHost/Extensions/MigrationExtensions.cs b/src/Aspire/MeAjudaAi.AppHost/Extensions/MigrationExtensions.cs
new file mode 100644
index 000000000..9e5c68572
--- /dev/null
+++ b/src/Aspire/MeAjudaAi.AppHost/Extensions/MigrationExtensions.cs
@@ -0,0 +1,356 @@
+using System.Reflection;
+using Aspire.Hosting;
+using Aspire.Hosting.ApplicationModel;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.AppHost.Extensions;
+
+///
+/// Extensões para aplicar migrations automaticamente no Aspire
+///
+public static class MigrationExtensions
+{
+ ///
+ /// Adiciona e executa migrations de todos os módulos antes de iniciar a aplicação
+ ///
+ public static IResourceBuilder WithMigrations(
+ this IResourceBuilder builder) where T : IResourceWithConnectionString
+ {
+ builder.ApplicationBuilder.Services.TryAddEnumerable(
+ ServiceDescriptor.Singleton());
+ return builder;
+ }
+}
+
+///
+/// Hosted service que roda migrations na inicialização do AppHost
+///
+internal class MigrationHostedService : IHostedService
+{
+ private readonly ILogger _logger;
+
+ public MigrationHostedService(
+ ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("🔄 Iniciando migrations de todos os módulos...");
+
+ List dbContextTypes = new();
+
+ try
+ {
+ var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
+
+ // Skip migrations in test environments - they're managed by test infrastructure
+ if (environment.Equals("Testing", StringComparison.OrdinalIgnoreCase) ||
+ environment.Equals("Test", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogInformation("⏭️ Skipping migrations in {Environment} environment", environment);
+ return;
+ }
+
+ var isDevelopment = environment.Equals("Development", StringComparison.OrdinalIgnoreCase);
+
+ var connectionString = GetConnectionString();
+ if (string.IsNullOrEmpty(connectionString))
+ {
+ if (isDevelopment)
+ {
+ _logger.LogWarning("⚠️ Connection string not found in Development, skipping migrations");
+ return;
+ }
+ else
+ {
+ _logger.LogError("❌ Connection string is required for migrations in {Environment} environment. " +
+ "Configure POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD.", environment);
+ throw new InvalidOperationException(
+ $"Missing database connection configuration for {environment} environment. " +
+ "Migrations cannot proceed without valid connection string.");
+ }
+ }
+
+ dbContextTypes = DiscoverDbContextTypes();
+ _logger.LogInformation("📋 Encontrados {Count} DbContexts para migração", dbContextTypes.Count);
+
+ foreach (var contextType in dbContextTypes)
+ {
+ await MigrateDbContextAsync(contextType, connectionString, cancellationToken);
+ }
+
+ _logger.LogInformation("✅ Todas as migrations foram aplicadas com sucesso!");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "❌ Erro ao aplicar migrations para {DbContextCount} módulo(s)", dbContextTypes.Count);
+ throw new InvalidOperationException(
+ $"Failed to apply database migrations for {dbContextTypes.Count} module(s)",
+ ex);
+ }
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
+
+ private string? GetConnectionString()
+ {
+ // Obter de variáveis de ambiente (padrão Aspire)
+ var host = Environment.GetEnvironmentVariable("POSTGRES_HOST")
+ ?? Environment.GetEnvironmentVariable("DB_HOST");
+ var port = Environment.GetEnvironmentVariable("POSTGRES_PORT")
+ ?? Environment.GetEnvironmentVariable("DB_PORT");
+ var database = Environment.GetEnvironmentVariable("POSTGRES_DB")
+ ?? Environment.GetEnvironmentVariable("MAIN_DATABASE");
+ var username = Environment.GetEnvironmentVariable("POSTGRES_USER")
+ ?? Environment.GetEnvironmentVariable("DB_USERNAME");
+ var password = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD")
+ ?? Environment.GetEnvironmentVariable("DB_PASSWORD");
+
+ // Para ambiente de desenvolvimento local apenas, permitir valores padrão
+ // NUNCA use valores padrão em produção - configure variáveis de ambiente adequadamente
+ var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
+ var isDevelopment = environment.Equals("Development", StringComparison.OrdinalIgnoreCase);
+
+ if (isDevelopment)
+ {
+ // Valores padrão APENAS para desenvolvimento local
+ // Use .env file ou user secrets para senha
+ host ??= "localhost";
+ port ??= "5432";
+ database ??= "meajudaai";
+ username ??= "postgres";
+ // Senha é obrigatória mesmo em dev - use variável de ambiente
+ if (string.IsNullOrEmpty(password))
+ {
+ _logger.LogWarning(
+ "POSTGRES_PASSWORD not set for Development environment. " +
+ "Set the environment variable or use user secrets.");
+ return null;
+ }
+
+ _logger.LogWarning(
+ "Using default connection values for Development environment. " +
+ "Configure environment variables for production deployments.");
+ }
+ else
+ {
+ // Em ambientes não-dev, EXIGIR configuração explícita
+ if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port) ||
+ string.IsNullOrEmpty(database) || string.IsNullOrEmpty(username) ||
+ string.IsNullOrEmpty(password))
+ {
+ _logger.LogError(
+ "Missing required database connection configuration. " +
+ "Set POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, and POSTGRES_PASSWORD " +
+ "environment variables.");
+ return null; // Falhar startup para evitar conexão insegura
+ }
+ }
+
+ return $"Host={host};Port={port};Database={database};Username={username};Password={password};Timeout=30;Command Timeout=60";
+ }
+
+ private List DiscoverDbContextTypes()
+ {
+ var dbContextTypes = new List();
+
+ // Primeiro, tentar carregar assemblies dos módulos dinamicamente
+ LoadModuleAssemblies();
+
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.FullName?.Contains("MeAjudaAi.Modules") == true)
+ .ToList();
+
+ if (assemblies.Count == 0)
+ {
+ _logger.LogWarning("⚠️ Nenhum assembly de módulo foi encontrado. Migrations não serão aplicadas automaticamente.");
+ return dbContextTypes;
+ }
+
+ foreach (var assembly in assemblies)
+ {
+ try
+ {
+ var types = assembly.GetTypes()
+ .Where(t => t.IsClass && !t.IsAbstract && typeof(DbContext).IsAssignableFrom(t))
+ .Where(t => t.Name.EndsWith("DbContext"))
+ .ToList();
+
+ dbContextTypes.AddRange(types);
+
+ if (types.Count > 0)
+ {
+ _logger.LogDebug("✅ Descobertos {Count} DbContext(s) em {Assembly}", types.Count, assembly.GetName().Name);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "⚠️ Erro ao descobrir tipos no assembly {AssemblyName}", assembly.FullName);
+ }
+ }
+
+ return dbContextTypes;
+ }
+
+ private void LoadModuleAssemblies()
+ {
+ try
+ {
+ var baseDirectory = AppContext.BaseDirectory;
+ var modulePattern = "MeAjudaAi.Modules.*.Infrastructure.dll";
+ var moduleDlls = Directory.GetFiles(baseDirectory, modulePattern, SearchOption.AllDirectories);
+
+ _logger.LogDebug("🔍 Procurando por assemblies de módulos em: {BaseDirectory}", baseDirectory);
+ _logger.LogDebug("📦 Encontrados {Count} DLLs de infraestrutura de módulos", moduleDlls.Length);
+
+ foreach (var dllPath in moduleDlls)
+ {
+ try
+ {
+ var assemblyName = AssemblyName.GetAssemblyName(dllPath);
+
+ // Verificar se já está carregado
+ if (AppDomain.CurrentDomain.GetAssemblies().Any(a => a.FullName == assemblyName.FullName))
+ {
+ _logger.LogDebug("⏭️ Assembly já carregado: {AssemblyName}", assemblyName.Name);
+ continue;
+ }
+
+ System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath);
+ _logger.LogDebug("✅ Assembly carregado: {AssemblyName}", assemblyName.Name);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "⚠️ Não foi possível carregar assembly: {DllPath}", Path.GetFileName(dllPath));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "⚠️ Erro ao tentar carregar assemblies de módulos dinamicamente");
+ }
+ }
+
+ private async Task MigrateDbContextAsync(Type contextType, string connectionString, CancellationToken cancellationToken)
+ {
+ var moduleName = ExtractModuleName(contextType);
+ _logger.LogInformation("🔧 Aplicando migrations para {Module}...", moduleName);
+
+ try
+ {
+ // Criar DbContextOptionsBuilder dinâmicamente mantendo tipo genérico
+ var optionsBuilderType = typeof(DbContextOptionsBuilder<>).MakeGenericType(contextType);
+ var optionsBuilderInstance = Activator.CreateInstance(optionsBuilderType);
+
+ if (optionsBuilderInstance == null)
+ {
+ throw new InvalidOperationException($"Não foi possível criar DbContextOptionsBuilder para {contextType.Name}");
+ }
+
+ // Configurar PostgreSQL - usar dynamic para simplificar reflexão
+ dynamic optionsBuilderDynamic = optionsBuilderInstance;
+
+ // Safe assembly name: FullName can be null for some assemblies
+ var assemblyName = contextType.Assembly.FullName
+ ?? contextType.Assembly.GetName().Name
+ ?? contextType.Assembly.ToString();
+
+ // Chamar UseNpgsql com connection string
+ Microsoft.EntityFrameworkCore.NpgsqlDbContextOptionsBuilderExtensions.UseNpgsql(
+ optionsBuilderDynamic,
+ connectionString,
+ (Action)(npgsqlOptions =>
+ {
+ npgsqlOptions.MigrationsAssembly(assemblyName);
+ npgsqlOptions.EnableRetryOnFailure(maxRetryCount: 3);
+ })
+ );
+
+ // Obter Options com tipo correto via reflection
+ var optionsProperty = optionsBuilderType.GetProperty("Options");
+ if (optionsProperty == null)
+ {
+ throw new InvalidOperationException(
+ $"Could not find 'Options' property on DbContextOptionsBuilder<{contextType.Name}>. " +
+ "This indicates a version mismatch or reflection issue.");
+ }
+
+ var options = optionsProperty.GetValue(optionsBuilderInstance);
+ if (options == null)
+ {
+ throw new InvalidOperationException(
+ $"DbContextOptions for {contextType.Name} is null after configuration. " +
+ "Ensure UseNpgsql was called successfully.");
+ }
+
+ // Verify constructor exists before attempting instantiation
+ var constructor = contextType.GetConstructor(new[] { options.GetType() });
+ if (constructor == null)
+ {
+ throw new InvalidOperationException(
+ $"No suitable constructor found for {contextType.Name} that accepts {options.GetType().Name}. " +
+ "Ensure the DbContext has a constructor that accepts DbContextOptions.");
+ }
+
+ // Criar instância do DbContext
+ var contextInstance = Activator.CreateInstance(contextType, options);
+ var context = contextInstance as DbContext;
+
+ if (context == null)
+ {
+ throw new InvalidOperationException(
+ $"Failed to cast created instance to DbContext for type {contextType.Name}. " +
+ $"Created instance type: {contextInstance?.GetType().Name ?? "null"}");
+ }
+
+ using (context)
+ {
+ // Aplicar migrations
+ var pendingMigrations = (await context.Database.GetPendingMigrationsAsync(cancellationToken)).ToList();
+
+ if (pendingMigrations.Any())
+ {
+ _logger.LogInformation("📦 {Module}: {Count} migrations pendentes", moduleName, pendingMigrations.Count);
+ foreach (var migration in pendingMigrations)
+ {
+ _logger.LogDebug(" - {Migration}", migration);
+ }
+
+ await context.Database.MigrateAsync(cancellationToken);
+ _logger.LogInformation("✅ {Module}: Migrations aplicadas com sucesso", moduleName);
+ }
+ else
+ {
+ _logger.LogInformation("✓ {Module}: Nenhuma migration pendente", moduleName);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "❌ Erro ao aplicar migrations para {Module}", moduleName);
+ throw new InvalidOperationException(
+ $"Failed to apply database migrations for module '{moduleName}' (DbContext: {contextType.Name})",
+ ex);
+ }
+ }
+
+ private static string ExtractModuleName(Type contextType)
+ {
+ // Extrai nome do módulo do namespace (ex: MeAjudaAi.Modules.Users.Infrastructure.Persistence.UsersDbContext -> Users)
+ var namespaceParts = contextType.Namespace?.Split('.') ?? Array.Empty();
+ var moduleIndex = Array.IndexOf(namespaceParts, "Modules");
+
+ if (moduleIndex >= 0 && moduleIndex + 1 < namespaceParts.Length)
+ {
+ return namespaceParts[moduleIndex + 1];
+ }
+
+ return contextType.Name.Replace("DbContext", "");
+ }
+}
diff --git a/src/Aspire/MeAjudaAi.AppHost/MeAjudaAi.AppHost.csproj b/src/Aspire/MeAjudaAi.AppHost/MeAjudaAi.AppHost.csproj
index 52d2ab7ff..0fa3afd7b 100644
--- a/src/Aspire/MeAjudaAi.AppHost/MeAjudaAi.AppHost.csproj
+++ b/src/Aspire/MeAjudaAi.AppHost/MeAjudaAi.AppHost.csproj
@@ -21,10 +21,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Aspire/MeAjudaAi.AppHost/Program.cs b/src/Aspire/MeAjudaAi.AppHost/Program.cs
index 133937b17..ddf2adc18 100644
--- a/src/Aspire/MeAjudaAi.AppHost/Program.cs
+++ b/src/Aspire/MeAjudaAi.AppHost/Program.cs
@@ -64,6 +64,9 @@ private static void ConfigureTestingEnvironment(IDistributedApplicationBuilder b
options.Password = testDbPassword;
});
+ // Aplicar migrations automaticamente (Testing também)
+ postgresql.MainDatabase.WithMigrations();
+
var redis = builder.AddRedis("redis");
_ = builder.AddProject("apiservice")
@@ -107,6 +110,9 @@ private static void ConfigureDevelopmentEnvironment(IDistributedApplicationBuild
options.IncludePgAdmin = includePgAdmin;
});
+ // Aplicar migrations automaticamente
+ postgresql.MainDatabase.WithMigrations();
+
var redis = builder.AddRedis("redis");
var rabbitMq = builder.AddRabbitMQ("rabbitmq");
@@ -180,10 +186,7 @@ private static void ConfigureProductionEnvironment(IDistributedApplicationBuilde
var keycloak = builder.AddMeAjudaAiKeycloakProduction();
- // TODO: Verificar se AddAzureContainerAppEnvironment está disponível na versão atual do Aspire
- // builder.AddAzureContainerAppEnvironment("cae");
-
- var apiService = builder.AddProject("apiservice")
+ builder.AddProject("apiservice")
.WithReference(postgresql.MainDatabase, "DefaultConnection")
.WithReference(redis)
.WaitFor(postgresql.MainDatabase)
diff --git a/src/Aspire/MeAjudaAi.AppHost/packages.lock.json b/src/Aspire/MeAjudaAi.AppHost/packages.lock.json
index ca639a4b8..a80f5bdb6 100644
--- a/src/Aspire/MeAjudaAi.AppHost/packages.lock.json
+++ b/src/Aspire/MeAjudaAi.AppHost/packages.lock.json
@@ -351,6 +351,29 @@
"Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0"
}
},
+ "Microsoft.EntityFrameworkCore": {
+ "type": "Direct",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "QsXvZ8G7Dl7x09rlq0b2dye7QqfReMq8yGdl7Mffi3Ip+aTa+JUMixBZ4lhCs9Ygjz2e9tiUACstxI+ADkwaFg==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore.Abstractions": "10.0.1",
+ "Microsoft.EntityFrameworkCore.Analyzers": "10.0.1",
+ "Microsoft.Extensions.Caching.Memory": "10.0.1",
+ "Microsoft.Extensions.Logging": "10.0.1"
+ }
+ },
+ "Npgsql.EntityFrameworkCore.PostgreSQL": {
+ "type": "Direct",
+ "requested": "[10.0.0, )",
+ "resolved": "10.0.0",
+ "contentHash": "E2+uSWxSB8LdsUVwPaqRWOcGOP92biry2JEwc0KJMdLJF+aZdczeIdEXVwEyv4nSVMQJH0o8tLhyAMiR6VF0lw==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "[10.0.0, 11.0.0)",
+ "Microsoft.EntityFrameworkCore.Relational": "[10.0.0, 11.0.0)",
+ "Npgsql": "10.0.0"
+ }
+ },
"SonarAnalyzer.CSharp": {
"type": "Direct",
"requested": "[10.15.0.120848, )",
@@ -803,6 +826,28 @@
"resolved": "8.0.0",
"contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw=="
},
+ "Microsoft.EntityFrameworkCore.Abstractions": {
+ "type": "Transitive",
+ "resolved": "10.0.1",
+ "contentHash": "sp6Uq8Oc3RVGtmxLS3ZgzVeFHrpLNymsMudoeqRmB9pRTWgvq2S903sF5OnaaZmh4Bz6kpq7FwofE+DOhKJYvg=="
+ },
+ "Microsoft.EntityFrameworkCore.Analyzers": {
+ "type": "Transitive",
+ "resolved": "10.0.1",
+ "contentHash": "Ug2lxkiz1bpnxQF/xdswLl5EBA6sAG2ig5nMjmtpQZO0C88ZnvUkbpH2vQq+8ultIRmvp5Ec2jndLGRMPjW0Ew=="
+ },
+ "Microsoft.Extensions.Caching.Memory": {
+ "type": "Transitive",
+ "resolved": "10.0.1",
+ "contentHash": "NxqSP0Ky4dZ5ybszdZCqs1X2C70s+dXflqhYBUh/vhcQVTIooNCXIYnLVbafoAFGZMs51d9+rHxveXs0ZC3SQQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Caching.Abstractions": "10.0.1",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1",
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.1",
+ "Microsoft.Extensions.Options": "10.0.1",
+ "Microsoft.Extensions.Primitives": "10.0.1"
+ }
+ },
"Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive",
"resolved": "10.0.1",
@@ -999,10 +1044,10 @@
},
"Npgsql": {
"type": "Transitive",
- "resolved": "8.0.3",
- "contentHash": "6WEmzsQJCZAlUG1pThKg/RmeF6V+I0DmBBBE/8YzpRtEzhyZzKcK7ulMANDm5CkxrALBEC8H+5plxHWtIL7xnA==",
+ "resolved": "10.0.0",
+ "contentHash": "xZAYhPOU2rUIFpV48xsqhCx9vXs6Y+0jX2LCoSEfDFYMw9jtAOUk3iQsCnDLrFIv9NT3JGMihn7nnuZsPKqJmA==",
"dependencies": {
- "Microsoft.Extensions.Logging.Abstractions": "8.0.0"
+ "Microsoft.Extensions.Logging.Abstractions": "10.0.0"
}
},
"Pipelines.Sockets.Unofficial": {
@@ -1084,6 +1129,27 @@
"resolved": "12.1.1",
"contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw=="
},
+ "Microsoft.EntityFrameworkCore.Relational": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "8/5kYGwKN6wtc89QqcPTOZDAJSMX8MzKCf5OmYjIfAHWTfsUEpGKYrdtfNk4X36rQ0BiU3n57Y4rbtnerzJN0Q==",
+ "dependencies": {
+ "Microsoft.EntityFrameworkCore": "10.0.1",
+ "Microsoft.Extensions.Caching.Memory": "10.0.1",
+ "Microsoft.Extensions.Configuration.Abstractions": "10.0.1",
+ "Microsoft.Extensions.Logging": "10.0.1"
+ }
+ },
+ "Microsoft.Extensions.Caching.Abstractions": {
+ "type": "CentralTransitive",
+ "requested": "[10.0.1, )",
+ "resolved": "10.0.1",
+ "contentHash": "Vb1vVAQDxHpXVdL9fpOX2BzeV7bbhzG4pAcIKRauRl0/VfkE8mq0f+fYC+gWICh3dlzTZInJ/cTeBS2MgU/XvQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "10.0.1"
+ }
+ },
"Microsoft.Extensions.Configuration": {
"type": "CentralTransitive",
"requested": "[10.0.1, )",
diff --git a/src/Aspire/MeAjudaAi.ServiceDefaults/Extensions.cs b/src/Aspire/MeAjudaAi.ServiceDefaults/Extensions.cs
index d4651c197..0143432ed 100644
--- a/src/Aspire/MeAjudaAi.ServiceDefaults/Extensions.cs
+++ b/src/Aspire/MeAjudaAi.ServiceDefaults/Extensions.cs
@@ -27,9 +27,6 @@ public static TBuilder AddServiceDefaults(this TBuilder builder) where
builder.AddFeatureManagement();
- // Service discovery not available for .NET 10 yet
- // builder.Services.AddServiceDiscovery();
-
builder.ConfigureHttpClients();
return builder;
@@ -81,8 +78,6 @@ private static TBuilder ConfigureHttpClients(this TBuilder builder)
builder.Services.ConfigureHttpClientDefaults(http =>
{
http.AddStandardResilienceHandler();
- // Service discovery not available for .NET 10 yet
- // http.AddServiceDiscovery();
http.ConfigureHttpClient(client =>
{
client.Timeout = TimeSpan.FromSeconds(30);
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/DocumentationExtensions.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/DocumentationExtensions.cs
index eadde00c3..057e6ce37 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/DocumentationExtensions.cs
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/DocumentationExtensions.cs
@@ -106,7 +106,7 @@ API para gerenciamento de usuários e prestadores de serviço.
// Último recurso: usar segmentos da rota
var segments = (apiDesc.RelativePath ?? "unknown")
.Split('/')
- .Where(s => !string.IsNullOrWhiteSpace(s) && !s.StartsWith("{"))
+ .Where(s => !string.IsNullOrWhiteSpace(s) && !s.StartsWith('{'))
.ToArray();
var ctrl = segments.FirstOrDefault() ?? "Api";
@@ -114,14 +114,8 @@ API para gerenciamento de usuários e prestadores de serviço.
return $"{ctrl}_{act}_{method}";
});
- // TODO: Reativar após migração para Swashbuckle 10.x completar
- // ExampleSchemaFilter precisa ser adaptado para IOpenApiSchema (read-only Example property)
- // Exemplos automáticos baseados em annotations
- // options.SchemaFilter();
-
// Filtros essenciais
options.OperationFilter();
- options.DocumentFilter();
});
return services;
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/PerformanceExtensions.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/PerformanceExtensions.cs
index 7e6bfc211..e8e27c912 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/PerformanceExtensions.cs
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/PerformanceExtensions.cs
@@ -132,22 +132,20 @@ private static bool HasSensitiveCookies(HttpRequest request, HttpResponse respon
};
// Verifica cookies na requisição
- foreach (var cookie in request.Cookies)
+ if (request.Cookies.Any(cookie =>
+ sensitiveCookieNames.Any(name =>
+ cookie.Key.Contains(name, StringComparison.OrdinalIgnoreCase))))
{
- if (sensitiveCookieNames.Any(name =>
- cookie.Key.Contains(name, StringComparison.OrdinalIgnoreCase)))
- return true;
+ return true;
}
// Verifica cookies sendo definidos na resposta
- if (response.Headers.TryGetValue("Set-Cookie", out var setCookies))
+ if (response.Headers.TryGetValue("Set-Cookie", out var setCookies) &&
+ setCookies.Any(setCookie =>
+ setCookie != null && sensitiveCookieNames.Any(name =>
+ setCookie.Contains(name, StringComparison.OrdinalIgnoreCase))))
{
- foreach (var setCookie in setCookies)
- {
- if (setCookie != null && sensitiveCookieNames.Any(name =>
- setCookie.Contains(name, StringComparison.OrdinalIgnoreCase)))
- return true;
- }
+ return true;
}
return false;
@@ -207,7 +205,7 @@ public Stream CreateStream(Stream outputStream)
return new GZipStream(outputStream, CompressionLevel.Optimal, leaveOpen: false);
}
- public bool ShouldCompressResponse(HttpContext context)
+ public static bool ShouldCompressResponse(HttpContext context)
{
return PerformanceExtensions.IsSafeForCompression(context);
}
@@ -226,7 +224,7 @@ public Stream CreateStream(Stream outputStream)
return new BrotliStream(outputStream, CompressionLevel.Optimal, leaveOpen: false);
}
- public bool ShouldCompressResponse(HttpContext context)
+ public static bool ShouldCompressResponse(HttpContext context)
{
return PerformanceExtensions.IsSafeForCompression(context);
}
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/ServiceCollectionExtensions.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/ServiceCollectionExtensions.cs
index 15efa83ac..fac4da911 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/Extensions/ServiceCollectionExtensions.cs
@@ -76,6 +76,9 @@ public static IServiceCollection AddApiServices(
// Adiciona serviços de autorização
services.AddAuthorizationPolicies();
+ // Adiciona suporte a ProblemDetails para respostas de erro padronizadas
+ services.AddProblemDetails();
+
// Otimizações de performance
services.AddResponseCompression();
services.AddStaticFilesWithCaching();
@@ -91,6 +94,9 @@ public static IApplicationBuilder UseApiServices(
this IApplicationBuilder app,
IWebHostEnvironment environment)
{
+ // Exception handling DEVE estar no início do pipeline
+ app.UseExceptionHandler();
+
// Middlewares de performance devem estar no início do pipeline
app.UseResponseCompression();
app.UseResponseCaching();
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Filters/ExampleSchemaFilter.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Filters/ExampleSchemaFilter.cs
deleted file mode 100644
index a1b987b62..000000000
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Filters/ExampleSchemaFilter.cs
+++ /dev/null
@@ -1,283 +0,0 @@
-using System.ComponentModel;
-using System.Reflection;
-using System.Text.Json.Nodes;
-using Microsoft.OpenApi;
-using Swashbuckle.AspNetCore.SwaggerGen;
-
-namespace MeAjudaAi.ApiService.Filters;
-
-// TODO: Migrar para Swashbuckle 10.x - IOpenApiSchema.Example é read-only
-// SOLUÇÃO: Usar reflexão para acessar propriedade Example na implementação concreta
-// Exemplo: schema.GetType().GetProperty("Example")?.SetValue(schema, exampleValue, null);
-// Temporariamente desabilitado em DocumentationExtensions.cs
-
-#pragma warning disable IDE0051, IDE0060 // Remove unused private members
-
-///
-/// Filtro para adicionar exemplos automáticos aos schemas baseado em atributos
-///
-public class ExampleSchemaFilter : ISchemaFilter
-{
- public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
- {
- // Swashbuckle 10.x: IOpenApiSchema.Example é read-only
- // SOLUÇÃO QUANDO REATIVAR: Usar reflexão para acessar implementação concreta
- // var exampleProp = schema.GetType().GetProperty("Example");
- // if (exampleProp?.CanWrite == true) exampleProp.SetValue(schema, value, null);
- throw new NotImplementedException("Precisa migração para Swashbuckle 10.x - usar reflexão para Example");
-
- /*
- // Adicionar exemplos baseados em DefaultValueAttribute
- if (context.Type.IsClass && context.Type != typeof(string))
- {
- AddExamplesFromProperties(schema, context.Type);
- }
-
- // Adicionar exemplos para enums
- if (context.Type.IsEnum)
- {
- AddEnumExamples(schema, context.Type);
- }
-
- // Adicionar descrições mais detalhadas
- AddDetailedDescription(schema, context.Type);
- */
- }
-
- private void AddExamplesFromProperties(IOpenApiSchema schema, Type type)
- {
- /*
- if (schema.Properties == null) return;
-
- var example = new JsonObject();
- var hasExamples = false;
-
- foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
- {
- var attrName = property.GetCustomAttribute()?.Name;
- var candidates = new[]
- {
- attrName,
- property.Name,
- char.ToLowerInvariant(property.Name[0]) + property.Name.Substring(1)
- }.Where(n => !string.IsNullOrEmpty(n)).Cast();
-
- var schemaKey = schema.Properties.Keys
- .FirstOrDefault(k => candidates.Any(c => string.Equals(k, c, StringComparison.Ordinal)));
- if (schemaKey == null) continue;
-
- var exampleValue = GetPropertyExample(property);
- if (exampleValue != null)
- {
- example[schemaKey] = exampleValue;
- hasExamples = true;
- }
- }
-
- if (hasExamples)
- {
- // OpenApiSchema.Example accepts object, so use JsonNode directly
- schema.Example = example;
- }
- */
- }
-
- private JsonNode? GetPropertyExample(PropertyInfo property)
- {
- /*
- // Verificar atributo DefaultValue primeiro
- var defaultValueAttr = property.GetCustomAttribute();
- if (defaultValueAttr != null)
- {
- var convertedValue = ConvertToJsonNode(defaultValueAttr.Value);
- if (convertedValue != null)
- {
- return convertedValue;
- }
-
- // Fallback para enums: tentar ToString() se a conversão retornou null
- if (defaultValueAttr.Value?.GetType().IsEnum == true)
- {
- return JsonValue.Create(defaultValueAttr.Value.ToString());
- }
-
- // Se não conseguiu converter, continua com a lógica baseada em tipo/nome
- }
-
- // Exemplos baseados no tipo e nome da propriedade
- var propertyName = property.Name.ToLowerInvariant();
- var propertyType = property.PropertyType;
-
- // Tratar tipos nullable
- if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
- {
- propertyType = Nullable.GetUnderlyingType(propertyType)!;
- }
-
- // Tratar tipos enum
- if (propertyType.IsEnum)
- {
- var enumNames = Enum.GetNames(propertyType);
- if (enumNames.Length > 0)
- {
- return JsonValue.Create(enumNames[0]);
- }
- }
-
- return propertyType.Name switch
- {
- nameof(String) => GetStringExample(propertyName),
- nameof(Guid) => JsonValue.Create("3fa85f64-5717-4562-b3fc-2c963f66afa6"),
- nameof(DateTime) => JsonValue.Create(new DateTime(2024, 01, 15, 10, 30, 00, DateTimeKind.Utc)),
- nameof(DateTimeOffset) => JsonValue.Create(new DateTimeOffset(2024, 01, 15, 10, 30, 00, TimeSpan.Zero)),
- nameof(Int32) => JsonValue.Create(GetIntegerExample(propertyName)),
- nameof(Int64) => JsonValue.Create(GetLongExample(propertyName)),
- nameof(Boolean) => JsonValue.Create(GetBooleanExample(propertyName)),
- nameof(Decimal) => JsonValue.Create(GetDecimalExample(propertyName)),
- nameof(Double) => JsonValue.Create(GetDoubleExample(propertyName)),
- _ => null
- };
- */
- return null;
- }
-
- private static JsonNode GetStringExample(string propertyName)
- {
- /*
- return propertyName switch
- {
- var name when name.Contains("email") => JsonValue.Create("usuario@example.com"),
- var name when name.Contains("phone") || name.Contains("telefone") => JsonValue.Create("+55 11 99999-9999"),
- var name when name.Contains("username") => JsonValue.Create("joao.silva"),
- var name when name.Contains("firstname") => JsonValue.Create("João"),
- var name when name.Contains("lastname") => JsonValue.Create("Silva"),
- var name when name.Contains("name") || name.Contains("nome") => JsonValue.Create("João Silva"),
- var name when name.Contains("password") => JsonValue.Create("MinhaSenh@123"),
- var name when name.Contains("description") || name.Contains("descricao") => JsonValue.Create("Descrição do item"),
- var name when name.Contains("title") || name.Contains("titulo") => JsonValue.Create("Título do Item"),
- var name when name.Contains("address") || name.Contains("endereco") => JsonValue.Create("Rua das Flores, 123"),
- var name when name.Contains("city") || name.Contains("cidade") => JsonValue.Create("São Paulo"),
- var name when name.Contains("state") || name.Contains("estado") => JsonValue.Create("SP"),
- var name when name.Contains("zipcode") || name.Contains("cep") => JsonValue.Create("01234-567"),
- var name when name.Contains("country") || name.Contains("pais") => JsonValue.Create("Brasil"),
- _ => JsonValue.Create("exemplo")
- };
- */
- return JsonValue.Create("exemplo");
- }
-
- private static int GetIntegerExample(string propertyName)
- {
- /*
- return propertyName switch
- {
- var name when name.Contains("age") || name.Contains("idade") => 30,
- var name when name.Contains("count") || name.Contains("quantity") => 10,
- var name when name.Contains("page") => 1,
- var name when name.Contains("size") || name.Contains("limit") => 20,
- var name when name.Contains("year") || name.Contains("ano") => DateTime.Now.Year,
- var name when name.Contains("month") || name.Contains("mes") => DateTime.Now.Month,
- var name when name.Contains("day") || name.Contains("dia") => DateTime.Now.Day,
- _ => 1
- };
- */
- return 1;
- }
-
- private static long GetLongExample(string propertyName)
- {
- /*
- return propertyName switch
- {
- var name when name.Contains("timestamp") => DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
- _ => 1L
- };
- */
- return 1L;
- }
-
- private static bool GetBooleanExample(string propertyName)
- {
- /*
- return propertyName switch
- {
- var name when name.Contains("active") || name.Contains("ativo") => true,
- var name when name.Contains("enabled") || name.Contains("habilitado") => true,
- var name when name.Contains("verified") || name.Contains("verificado") => true,
- var name when name.Contains("deleted") || name.Contains("excluido") => false,
- var name when name.Contains("disabled") || name.Contains("desabilitado") => false,
- _ => true
- };
- */
- return true;
- }
-
- private static double GetDecimalExample(string propertyName)
- {
- /*
- return propertyName switch
- {
- var name when name.Contains("price") || name.Contains("preco") => 99.99,
- var name when name.Contains("rate") || name.Contains("taxa") => 4.5,
- var name when name.Contains("percentage") || name.Contains("porcentagem") => 15.0,
- _ => 1.0
- };
- */
- return 1.0;
- }
-
- private double GetDoubleExample(string propertyName)
- {
- return GetDecimalExample(propertyName);
- }
-
- private static void AddEnumExamples(IOpenApiSchema schema, Type enumType)
- {
- /*
- var enumValues = Enum.GetValues(enumType);
- if (enumValues.Length == 0) return;
-
- var firstValue = enumValues.GetValue(0);
- if (firstValue == null) return;
-
- // Use reflexão para definir Example (read-only na interface)
- var exampleProp = schema.GetType().GetProperty("Example");
- if (exampleProp?.CanWrite == true)
- {
- exampleProp.SetValue(schema, firstValue.ToString(), null);
- }
- */
- }
-
- private static void AddDetailedDescription(IOpenApiSchema schema, Type type)
- {
- /*
- var descriptionAttr = type.GetCustomAttribute();
- if (descriptionAttr != null && string.IsNullOrEmpty(schema.Description))
- {
- schema.Description = descriptionAttr.Description;
- }
- */
- }
-
- private static JsonNode? ConvertToJsonNode(object? value)
- {
- return value switch
- {
- null => null,
- string s => JsonValue.Create(s),
- int i => JsonValue.Create(i),
- long l => JsonValue.Create(l),
- float f => JsonValue.Create(f),
- double d => JsonValue.Create(d),
- decimal dec => JsonValue.Create(dec),
- bool b => JsonValue.Create(b),
- DateTime dt => JsonValue.Create(dt),
- DateTimeOffset dto => JsonValue.Create(dto),
- Guid g => JsonValue.Create(g.ToString()),
- _ => null // Unsupported type; return null instead of ToString() to avoid unexpected JSON
- };
- }
-}
-
-#pragma warning restore IDE0051, IDE0060
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Filters/ModuleTagsDocumentFilter.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Filters/ModuleTagsDocumentFilter.cs
deleted file mode 100644
index bfc0ce854..000000000
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Filters/ModuleTagsDocumentFilter.cs
+++ /dev/null
@@ -1,190 +0,0 @@
-using System.Text.Json.Nodes;
-using Microsoft.OpenApi;
-using Swashbuckle.AspNetCore.SwaggerGen;
-
-namespace MeAjudaAi.ApiService.Filters;
-
-///
-/// Filtro para organizar tags por módulos e adicionar descrições
-///
-public class ModuleTagsDocumentFilter : IDocumentFilter
-{
- private readonly Dictionary _moduleDescriptions = new()
- {
- ["Users"] = "Gerenciamento de usuários, perfis e autenticação",
- //["Services"] = "Catálogo de serviços e categorias",
- //["Bookings"] = "Agendamentos e execução de serviços",
- //["Notifications"] = "Sistema de notificações e comunicação",
- //["Reports"] = "Relatórios e analytics do sistema",
- //["Admin"] = "Funcionalidades administrativas do sistema",
- ["Health"] = "Monitoramento e health checks dos serviços"
- };
-
- public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
- {
- // Organizar tags em ordem lógica
- var orderedTags = new List { "Users",/* "Services", "Bookings", "Notifications", "Reports", "Admin",*/ "Health" };
-
- // Criar tags com descrições
- swaggerDoc.Tags = new HashSet();
-
- foreach (var tagName in orderedTags)
- {
- if (_moduleDescriptions.TryGetValue(tagName, out var description))
- {
- swaggerDoc.Tags.Add(new OpenApiTag
- {
- Name = tagName,
- Description = description
- });
- }
- }
-
- // Adicionar tags que não estão na lista pré-definida
- var usedTags = GetUsedTagsFromPaths(swaggerDoc);
- foreach (var tag in usedTags.Where(t => !orderedTags.Contains(t)))
- {
- swaggerDoc.Tags.Add(new OpenApiTag
- {
- Name = tag,
- Description = $"Operações relacionadas a {tag}"
- });
- }
-
- // Adicionar informações de servidor
- AddServerInformation(swaggerDoc);
-
- // Adicionar exemplos globais
- AddGlobalExamples(swaggerDoc);
- }
-
- private static HashSet GetUsedTagsFromPaths(OpenApiDocument swaggerDoc)
- {
- var tags = new HashSet(StringComparer.OrdinalIgnoreCase);
-
- // Guard against null Paths collection
- if (swaggerDoc.Paths == null)
- return tags;
-
- foreach (var path in swaggerDoc.Paths.Values)
- {
- // Guard against null path
- if (path?.Operations == null)
- continue;
-
- foreach (var operation in path.Operations.Values)
- {
- // Guard against null operation or Tags collection
- if (operation?.Tags == null)
- continue;
-
- foreach (var tag in operation.Tags)
- {
- // Skip tags with null or empty Name
- if (!string.IsNullOrEmpty(tag?.Name))
- {
- tags.Add(tag.Name);
- }
- }
- }
- }
-
- return tags;
- }
-
- private static void AddServerInformation(OpenApiDocument swaggerDoc)
- {
- // TODO(#TechDebt): Investigate OpenApiServer initialization issue in .NET 10 / Swashbuckle 10
- // Temporarily disabled to fix UriFormatException. Track restoration in backlog.
- // Related: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2816
- // swaggerDoc.Servers =
- // [
- // new OpenApiServer
- // {
- // Url = "http://localhost:5000",
- // Description = "Desenvolvimento Local"
- // },
- // new OpenApiServer
- // {
- // Url = "https://api.meajudaai.com",
- // Description = "Produção"
- // }
- // ];
- }
-
- private static void AddGlobalExamples(OpenApiDocument swaggerDoc)
- {
- // Adicionar componentes reutilizáveis
- swaggerDoc.Components ??= new OpenApiComponents();
-
- // Exemplo de erro padrão
- if (swaggerDoc.Components.Examples == null)
- swaggerDoc.Components.Examples = new Dictionary();
-
- swaggerDoc.Components.Examples["ErrorResponse"] = new OpenApiExample
- {
- Summary = "Resposta de Erro Padrão",
- Description = "Formato padrão das respostas de erro da API",
- Value = new JsonObject
- {
- ["type"] = "ValidationError",
- ["title"] = "Dados de entrada inválidos",
- ["status"] = 400,
- ["detail"] = "Um ou mais campos contêm valores inválidos",
- ["instance"] = "/api/v1/users",
- ["errors"] = new JsonObject
- {
- ["email"] = new JsonArray
- {
- "O campo Email é obrigatório",
- "Email deve ter um formato válido"
- }
- },
- ["traceId"] = "0HN7GKZB8K9QA:00000001"
- }
- };
-
- swaggerDoc.Components.Examples["SuccessResponse"] = new OpenApiExample
- {
- Summary = "Resposta de Sucesso Padrão",
- Description = "Formato padrão das respostas de sucesso da API",
- Value = new JsonObject
- {
- ["success"] = true,
- ["data"] = new JsonObject
- {
- ["id"] = "3fa85f64-5717-4562-b3fc-2c963f66afa6",
- ["createdAt"] = "2024-01-15T10:30:00Z"
- },
- ["metadata"] = new JsonObject
- {
- ["requestId"] = "req_abc123",
- ["version"] = "1.0",
- ["timestamp"] = "2024-01-15T10:30:00Z"
- }
- }
- };
-
- // Schemas reutilizáveis
- if (swaggerDoc.Components.Schemas == null)
- swaggerDoc.Components.Schemas = new Dictionary();
-
- swaggerDoc.Components.Schemas["PaginationMetadata"] = new OpenApiSchema
- {
- Type = JsonSchemaType.Object,
- Description = "Metadados de paginação para listagens",
- Properties = new Dictionary
- {
- ["page"] = new OpenApiSchema { Type = JsonSchemaType.Integer, Description = "Página atual (base 1)", Example = JsonValue.Create(1) },
- ["pageSize"] = new OpenApiSchema { Type = JsonSchemaType.Integer, Description = "Itens por página", Example = JsonValue.Create(20) },
- ["totalItems"] = new OpenApiSchema { Type = JsonSchemaType.Integer, Description = "Total de itens", Example = JsonValue.Create(150) },
- ["totalPages"] = new OpenApiSchema { Type = JsonSchemaType.Integer, Description = "Total de páginas", Example = JsonValue.Create(8) },
- ["hasNextPage"] = new OpenApiSchema { Type = JsonSchemaType.Boolean, Description = "Indica se há próxima página", Example = JsonValue.Create(true) },
- ["hasPreviousPage"] = new OpenApiSchema { Type = JsonSchemaType.Boolean, Description = "Indica se há página anterior", Example = JsonValue.Create(false) }
- },
- Required = new HashSet { "page", "pageSize", "totalItems", "totalPages", "hasNextPage", "hasPreviousPage" }
- };
- }
-}
-
-
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/GeographicRestrictionMiddleware.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/GeographicRestrictionMiddleware.cs
index f5e28786d..6a5f1383f 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/GeographicRestrictionMiddleware.cs
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/GeographicRestrictionMiddleware.cs
@@ -132,10 +132,8 @@ private static (string? City, string? State) ExtractLocation(HttpContext context
return (city, state);
}
- // TODO Sprint 2: Implementar GeoIP lookup baseado em IP
- // Opção 1: GeoIP (MaxMind, IP2Location)
- // var ip = context.Connection.RemoteIpAddress;
- // return await _geoIpService.GetLocationFromIpAsync(ip);
+ // TODO: Implementar GeoIP lookup baseado em IP para detectar localização automaticamente.
+ // Opções: MaxMind GeoIP2, IP2Location, ou IPGeolocation API.
return (null, null);
}
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs
index 35aeb1dd3..a60ba5a43 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/Middlewares/RateLimitingMiddleware.cs
@@ -96,21 +96,19 @@ private static int GetEffectiveLimit(HttpContext context, RateLimitOptions rateL
var requestPath = context.Request.Path.Value ?? string.Empty;
// 1. Check for endpoint-specific limits first
- foreach (var endpointLimit in rateLimitOptions.EndpointLimits)
+ var matchingLimit = rateLimitOptions.EndpointLimits
+ .FirstOrDefault(endpointLimit =>
+ IsPathMatch(requestPath, endpointLimit.Value.Pattern) &&
+ ((isAuthenticated && endpointLimit.Value.ApplyToAuthenticated) ||
+ (!isAuthenticated && endpointLimit.Value.ApplyToAnonymous)));
+
+ if (matchingLimit.Value != null)
{
- if (IsPathMatch(requestPath, endpointLimit.Value.Pattern))
- {
- // Check if this endpoint limit applies to the current user type
- if ((isAuthenticated && endpointLimit.Value.ApplyToAuthenticated) ||
- (!isAuthenticated && endpointLimit.Value.ApplyToAnonymous))
- {
- return ScaleToWindow(
- endpointLimit.Value.RequestsPerMinute,
- endpointLimit.Value.RequestsPerHour,
- 0,
- window);
- }
- }
+ return ScaleToWindow(
+ matchingLimit.Value.RequestsPerMinute,
+ matchingLimit.Value.RequestsPerHour,
+ 0,
+ window);
}
// 2. Check for role-specific limits (only for authenticated users)
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs
index ea27d9851..0ee4bf814 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs
@@ -9,11 +9,16 @@
using MeAjudaAi.ServiceDefaults;
using MeAjudaAi.Shared.Extensions;
using MeAjudaAi.Shared.Logging;
+using MeAjudaAi.Shared.Seeding;
using Serilog;
using Serilog.Context;
+namespace MeAjudaAi.ApiService;
+
public partial class Program
{
+ protected Program() { }
+
public static async Task Main(string[] args)
{
try
@@ -25,10 +30,9 @@ public static async Task Main(string[] args)
// Configurações via ServiceDefaults e Shared (sem duplicar Serilog)
builder.AddServiceDefaults();
builder.Services.AddHttpContextAccessor();
- builder.Services.AddSharedServices(builder.Configuration);
- builder.Services.AddApiServices(builder.Configuration, builder.Environment);
- // Registrar módulos
+ // Registrar módulos ANTES de AddSharedServices
+ // (exception handlers específicos devem ser registrados antes do global)
builder.Services.AddUsersModule(builder.Configuration);
builder.Services.AddProvidersModule(builder.Configuration);
builder.Services.AddDocumentsModule(builder.Configuration);
@@ -36,10 +40,17 @@ public static async Task Main(string[] args)
builder.Services.AddLocationModule(builder.Configuration);
builder.Services.AddServiceCatalogsModule(builder.Configuration);
+ // Shared services por último (GlobalExceptionHandler atua como fallback)
+ builder.Services.AddSharedServices(builder.Configuration);
+ builder.Services.AddApiServices(builder.Configuration, builder.Environment);
+
var app = builder.Build();
await ConfigureMiddlewareAsync(app);
+ // Seed dados de desenvolvimento se necessário
+ await app.SeedDevelopmentDataIfNeededAsync();
+
LogStartupComplete(app);
await app.RunAsync();
diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/packages.lock.json b/src/Bootstrapper/MeAjudaAi.ApiService/packages.lock.json
index b86018bf7..9fa7238cf 100644
--- a/src/Bootstrapper/MeAjudaAi.ApiService/packages.lock.json
+++ b/src/Bootstrapper/MeAjudaAi.ApiService/packages.lock.json
@@ -717,6 +717,7 @@
"type": "Project",
"dependencies": {
"MeAjudaAi.Modules.Providers.Domain": "[1.0.0, )",
+ "MeAjudaAi.Modules.ServiceCatalogs.Application": "[1.0.0, )",
"MeAjudaAi.Shared": "[1.0.0, )"
}
},
diff --git a/src/Modules/Documents/API/Extensions.cs b/src/Modules/Documents/API/Extensions.cs
index 19ab4c39a..6cb39acf0 100644
--- a/src/Modules/Documents/API/Extensions.cs
+++ b/src/Modules/Documents/API/Extensions.cs
@@ -85,7 +85,9 @@ private static void EnsureDatabaseMigrations(WebApplication app)
{
// Em produção, não fazer fallback silencioso - relançar para visão do problema
contextLogger?.LogError(ex, "Erro crítico ao aplicar migrações do módulo Documents em ambiente de produção.");
- throw;
+ throw new InvalidOperationException(
+ "Critical error applying Documents module database migrations in production environment",
+ ex);
}
}
}
diff --git a/src/Modules/Documents/Application/Handlers/UploadDocumentCommandHandler.cs b/src/Modules/Documents/Application/Handlers/UploadDocumentCommandHandler.cs
index 2e7a21c35..64f7dd2c0 100644
--- a/src/Modules/Documents/Application/Handlers/UploadDocumentCommandHandler.cs
+++ b/src/Modules/Documents/Application/Handlers/UploadDocumentCommandHandler.cs
@@ -131,12 +131,12 @@ await _backgroundJobService.EnqueueAsync(
catch (UnauthorizedAccessException ex)
{
_logger.LogWarning(ex, "Authorization failed while uploading document for provider {ProviderId}", command.ProviderId);
- throw; // Re-throw para middleware tratar com 401/403
+ throw;
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Validation failed while uploading document: {Message}", ex.Message);
- throw; // Re-throw para middleware tratar com 400
+ throw;
}
catch (Exception ex)
{
diff --git a/src/Modules/Documents/Application/ModuleApi/DocumentsModuleApi.cs b/src/Modules/Documents/Application/ModuleApi/DocumentsModuleApi.cs
index 9b4bca9e2..baa8325ba 100644
--- a/src/Modules/Documents/Application/ModuleApi/DocumentsModuleApi.cs
+++ b/src/Modules/Documents/Application/ModuleApi/DocumentsModuleApi.cs
@@ -75,9 +75,9 @@ public async Task IsAvailableAsync(CancellationToken cancellationToken = d
logger.LogDebug("Documents module is available and healthy");
return true;
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException ex)
{
- logger.LogDebug("Documents module availability check was cancelled");
+ logger.LogDebug(ex, "Documents module availability check was cancelled");
throw;
}
catch (Exception ex)
diff --git a/src/Modules/Documents/Infrastructure/Events/Handlers/DocumentVerifiedDomainEventHandler.cs b/src/Modules/Documents/Infrastructure/Events/Handlers/DocumentVerifiedDomainEventHandler.cs
index 50c52d8b6..668406581 100644
--- a/src/Modules/Documents/Infrastructure/Events/Handlers/DocumentVerifiedDomainEventHandler.cs
+++ b/src/Modules/Documents/Infrastructure/Events/Handlers/DocumentVerifiedDomainEventHandler.cs
@@ -57,7 +57,9 @@ public async Task HandleAsync(DocumentVerifiedDomainEvent domainEvent, Cancellat
ex,
"Error handling DocumentVerifiedDomainEvent for document {DocumentId}",
domainEvent.AggregateId);
- throw;
+ throw new InvalidOperationException(
+ $"Failed to handle DocumentVerifiedDomainEvent for document {domainEvent.AggregateId}",
+ ex);
}
}
}
diff --git a/src/Modules/Documents/Infrastructure/Services/AzureBlobStorageService.cs b/src/Modules/Documents/Infrastructure/Services/AzureBlobStorageService.cs
index 42ee4fe6d..6fb60037a 100644
--- a/src/Modules/Documents/Infrastructure/Services/AzureBlobStorageService.cs
+++ b/src/Modules/Documents/Infrastructure/Services/AzureBlobStorageService.cs
@@ -48,7 +48,9 @@ public class AzureBlobStorageService(BlobServiceClient blobServiceClient, ILogge
catch (RequestFailedException ex)
{
_logger.LogError(ex, "Erro ao gerar SAS token de upload para blob {BlobName}", blobName);
- throw;
+ throw new InvalidOperationException(
+ $"Failed to generate Azure Blob Storage SAS upload token for blob '{blobName}' (Status: {ex.Status})",
+ ex);
}
}
@@ -87,7 +89,9 @@ public class AzureBlobStorageService(BlobServiceClient blobServiceClient, ILogge
catch (RequestFailedException ex)
{
_logger.LogError(ex, "Erro ao gerar SAS token de download para blob {BlobName}", blobName);
- throw;
+ throw new InvalidOperationException(
+ $"Failed to generate Azure Blob Storage SAS download token for blob '{blobName}' (Status: {ex.Status})",
+ ex);
}
}
@@ -99,11 +103,17 @@ public async Task ExistsAsync(string blobName, CancellationToken cancellat
var response = await blobClient.ExistsAsync(cancellationToken);
return response.Value;
}
- catch (RequestFailedException ex)
+ catch (RequestFailedException ex) when (ex.Status == 404)
{
- _logger.LogError(ex, "Erro ao verificar existência do blob {BlobName}", blobName);
return false;
}
+ catch (RequestFailedException ex)
+ {
+ _logger.LogError(ex, "Erro ao verificar existência do blob {BlobName} (Status: {Status})", blobName, ex.Status);
+ throw new InvalidOperationException(
+ $"Failed to check existence of blob '{blobName}' (Status: {ex.Status})",
+ ex);
+ }
}
public async Task DeleteAsync(string blobName, CancellationToken cancellationToken = default)
@@ -117,7 +127,9 @@ public async Task DeleteAsync(string blobName, CancellationToken cancellationTok
catch (RequestFailedException ex)
{
_logger.LogError(ex, "Erro ao deletar blob {BlobName}", blobName);
- throw;
+ throw new InvalidOperationException(
+ $"Failed to delete blob '{blobName}' from Azure Blob Storage (Status: {ex.Status})",
+ ex);
}
}
}
diff --git a/src/Modules/Documents/Tests/Integration/DocumentsInfrastructureIntegrationTests.cs b/src/Modules/Documents/Tests/Integration/DocumentsInfrastructureIntegrationTests.cs
index 4cd16a135..96f542dc2 100644
--- a/src/Modules/Documents/Tests/Integration/DocumentsInfrastructureIntegrationTests.cs
+++ b/src/Modules/Documents/Tests/Integration/DocumentsInfrastructureIntegrationTests.cs
@@ -6,6 +6,7 @@
using MeAjudaAi.Modules.Documents.Infrastructure.Persistence;
using MeAjudaAi.Modules.Documents.Infrastructure.Persistence.Repositories;
using MeAjudaAi.Modules.Documents.Tests.Integration.Mocks;
+using MeAjudaAi.Shared.Time;
using Microsoft.EntityFrameworkCore;
namespace MeAjudaAi.Modules.Documents.Tests.Integration;
@@ -24,7 +25,7 @@ public class DocumentsInfrastructureIntegrationTests : IDisposable
public DocumentsInfrastructureIntegrationTests()
{
var options = new DbContextOptionsBuilder()
- .UseInMemoryDatabase($"DocumentsTestDb_{Guid.CreateVersion7()}")
+ .UseInMemoryDatabase($"DocumentsTestDb_{UuidGenerator.NewId()}")
.Options;
_dbContext = new DocumentsDbContext(options);
@@ -46,7 +47,7 @@ public async Task Repository_AddDocument_ShouldPersistToDatabase()
{
// Arrange
var document = Document.Create(
- Guid.CreateVersion7(),
+ UuidGenerator.NewId(),
EDocumentType.IdentityDocument,
"test-document.pdf",
"test-blob-path.pdf"
@@ -69,10 +70,10 @@ public async Task Repository_AddDocument_ShouldPersistToDatabase()
public async Task Repository_GetByProviderId_ShouldReturnAllDocuments()
{
// Arrange
- var providerId = Guid.CreateVersion7();
+ var providerId = UuidGenerator.NewId();
var doc1 = Document.Create(providerId, EDocumentType.IdentityDocument, "doc1.pdf", "path1.pdf");
var doc2 = Document.Create(providerId, EDocumentType.ProofOfResidence, "doc2.pdf", "path2.pdf");
- var doc3 = Document.Create(Guid.CreateVersion7(), EDocumentType.CriminalRecord, "doc3.pdf", "path3.pdf");
+ var doc3 = Document.Create(UuidGenerator.NewId(), EDocumentType.CriminalRecord, "doc3.pdf", "path3.pdf");
await _repository.AddAsync(doc1);
await _repository.AddAsync(doc2);
@@ -93,7 +94,7 @@ public async Task Repository_GetByProviderId_ShouldReturnAllDocuments()
public async Task Repository_UpdateDocument_ShouldPersistChanges()
{
// Arrange
- var document = Document.Create(Guid.CreateVersion7(), EDocumentType.IdentityDocument, "test.pdf", "path.pdf");
+ var document = Document.Create(UuidGenerator.NewId(), EDocumentType.IdentityDocument, "test.pdf", "path.pdf");
await _repository.AddAsync(document);
await _dbContext.SaveChangesAsync();
@@ -221,8 +222,8 @@ public async Task DocumentIntelligence_AnalyzeDocument_ShouldExtractFields()
public async Task CompleteWorkflow_UploadToVerification_ShouldWork()
{
// Arrange
- var providerId = Guid.CreateVersion7();
- var blobName = $"{providerId}/identity-{Guid.CreateVersion7()}.pdf";
+ var providerId = UuidGenerator.NewId();
+ var blobName = $"{providerId}/identity-{UuidGenerator.NewId()}.pdf";
// Act 1: Generate upload URL
var (uploadUrl, _) = await _blobStorageService.GenerateUploadUrlAsync(blobName, "application/pdf");
@@ -262,7 +263,7 @@ public async Task CompleteWorkflow_UploadToVerification_ShouldWork()
public async Task MultipleDocuments_ForSameProvider_ShouldBeManageable()
{
// Arrange
- var providerId = Guid.CreateVersion7();
+ var providerId = UuidGenerator.NewId();
var documentTypes = new[]
{
EDocumentType.IdentityDocument,
@@ -274,7 +275,7 @@ public async Task MultipleDocuments_ForSameProvider_ShouldBeManageable()
var documentIds = new List();
foreach (var docType in documentTypes)
{
- var blobName = $"{providerId}/{docType}-{Guid.CreateVersion7()}.pdf";
+ var blobName = $"{providerId}/{docType}-{UuidGenerator.NewId()}.pdf";
await _blobStorageService.GenerateUploadUrlAsync(blobName, "application/pdf");
var document = Document.Create(providerId, docType, $"{docType}.pdf", blobName);
@@ -296,7 +297,7 @@ public async Task MultipleDocuments_ForSameProvider_ShouldBeManageable()
public async Task DocumentVerificationFlow_WithRejection_ShouldWork()
{
// Arrange
- var document = Document.Create(Guid.CreateVersion7(), EDocumentType.IdentityDocument, "test.pdf", "path.pdf");
+ var document = Document.Create(UuidGenerator.NewId(), EDocumentType.IdentityDocument, "test.pdf", "path.pdf");
await _repository.AddAsync(document);
await _dbContext.SaveChangesAsync();
@@ -317,13 +318,13 @@ public async Task DocumentVerificationFlow_WithRejection_ShouldWork()
public async Task Repository_QueryByStatus_ShouldFilterCorrectly()
{
// Arrange
- var uploaded = Document.Create(Guid.CreateVersion7(), EDocumentType.IdentityDocument, "uploaded.pdf", "path1");
+ var uploaded = Document.Create(UuidGenerator.NewId(), EDocumentType.IdentityDocument, "uploaded.pdf", "path1");
- var verified = Document.Create(Guid.CreateVersion7(), EDocumentType.ProofOfResidence, "verified.pdf", "path2");
+ var verified = Document.Create(UuidGenerator.NewId(), EDocumentType.ProofOfResidence, "verified.pdf", "path2");
verified.MarkAsPendingVerification();
verified.MarkAsVerified("{\"data\":\"test\"}");
- var rejected = Document.Create(Guid.CreateVersion7(), EDocumentType.CriminalRecord, "rejected.pdf", "path3");
+ var rejected = Document.Create(UuidGenerator.NewId(), EDocumentType.CriminalRecord, "rejected.pdf", "path3");
rejected.MarkAsPendingVerification();
rejected.MarkAsRejected("Invalid");
@@ -346,8 +347,8 @@ public async Task Repository_QueryByStatus_ShouldFilterCorrectly()
public async Task DbContext_MultipleSaves_ShouldPersistBoth()
{
// Arrange
- var doc1 = Document.Create(Guid.CreateVersion7(), EDocumentType.IdentityDocument, "doc1.pdf", "path1");
- var doc2 = Document.Create(Guid.CreateVersion7(), EDocumentType.ProofOfResidence, "doc2.pdf", "path2");
+ var doc1 = Document.Create(UuidGenerator.NewId(), EDocumentType.IdentityDocument, "doc1.pdf", "path1");
+ var doc2 = Document.Create(UuidGenerator.NewId(), EDocumentType.ProofOfResidence, "doc2.pdf", "path2");
// Act
await _repository.AddAsync(doc1);
diff --git a/src/Modules/Documents/Tests/Unit/Application/UploadDocumentCommandHandlerTests.cs b/src/Modules/Documents/Tests/Unit/Application/UploadDocumentCommandHandlerTests.cs
index bdb8c4630..94f63b799 100644
--- a/src/Modules/Documents/Tests/Unit/Application/UploadDocumentCommandHandlerTests.cs
+++ b/src/Modules/Documents/Tests/Unit/Application/UploadDocumentCommandHandlerTests.cs
@@ -306,8 +306,10 @@ public async Task HandleAsync_WithInvalidDocumentType_ShouldThrowArgumentExcepti
102400);
// Act & Assert
- await Assert.ThrowsAsync(
+ var exception = await Assert.ThrowsAsync(
() => _handler.HandleAsync(command, CancellationToken.None));
+
+ exception.Message.Should().Contain("Tipo de documento inválido");
}
[Fact]
diff --git a/src/Modules/Documents/Tests/Unit/Infrastructure/Events/DocumentVerifiedDomainEventHandlerTests.cs b/src/Modules/Documents/Tests/Unit/Infrastructure/Events/DocumentVerifiedDomainEventHandlerTests.cs
index 4a7e1e139..e0b4e68c5 100644
--- a/src/Modules/Documents/Tests/Unit/Infrastructure/Events/DocumentVerifiedDomainEventHandlerTests.cs
+++ b/src/Modules/Documents/Tests/Unit/Infrastructure/Events/DocumentVerifiedDomainEventHandlerTests.cs
@@ -119,8 +119,10 @@ public async Task HandleAsync_WhenMessageBusThrows_ShouldLogErrorAndRethrow()
var act = async () => await _handler.HandleAsync(domainEvent);
// Assert
- await act.Should().ThrowAsync()
- .WithMessage("Message bus error");
+ var ex = await act.Should().ThrowAsync()
+ .WithMessage($"Failed to handle DocumentVerifiedDomainEvent for document {documentId}");
+ ex.Which.InnerException.Should().BeOfType();
+ ex.Which.InnerException!.Message.Should().Be("Message bus error");
VerifyLogMessageWithException(LogLevel.Error, $"Error handling DocumentVerifiedDomainEvent for document {documentId}", exception, Times.Once());
}
diff --git a/src/Modules/Documents/Tests/Unit/Infrastructure/Services/AzureBlobStorageServiceTests.cs b/src/Modules/Documents/Tests/Unit/Infrastructure/Services/AzureBlobStorageServiceTests.cs
index 5e9ef956d..54f1fc96f 100644
--- a/src/Modules/Documents/Tests/Unit/Infrastructure/Services/AzureBlobStorageServiceTests.cs
+++ b/src/Modules/Documents/Tests/Unit/Infrastructure/Services/AzureBlobStorageServiceTests.cs
@@ -81,7 +81,7 @@ await act.Should().ThrowAsync()
}
[Fact]
- public async Task GenerateUploadUrlAsync_WhenRequestFails_ShouldThrowRequestFailedException()
+ public async Task GenerateUploadUrlAsync_WhenRequestFails_ShouldThrowInvalidOperationException()
{
// Arrange
var blobName = "test-document.pdf";
@@ -99,7 +99,8 @@ public async Task GenerateUploadUrlAsync_WhenRequestFails_ShouldThrowRequestFail
var act = () => _service.GenerateUploadUrlAsync(blobName, contentType);
// Assert
- await act.Should().ThrowAsync();
+ var exception = await act.Should().ThrowAsync();
+ exception.And.InnerException.Should().BeOfType();
}
[Fact]
@@ -182,7 +183,7 @@ public async Task ExistsAsync_WhenBlobDoesNotExist_ShouldReturnFalse()
}
[Fact]
- public async Task ExistsAsync_WhenRequestFails_ShouldReturnFalse()
+ public async Task ExistsAsync_WhenRequestFails_ShouldThrowInvalidOperationException()
{
// Arrange
var blobName = "test-document.pdf";
@@ -192,10 +193,11 @@ public async Task ExistsAsync_WhenRequestFails_ShouldReturnFalse()
.ThrowsAsync(new RequestFailedException("Azure error"));
// Act
- var result = await _service.ExistsAsync(blobName);
+ var act = () => _service.ExistsAsync(blobName);
// Assert
- result.Should().BeFalse();
+ var exception = await act.Should().ThrowAsync();
+ exception.And.InnerException.Should().BeOfType();
}
[Fact]
@@ -225,7 +227,7 @@ public async Task DeleteAsync_WhenBlobExists_ShouldDeleteSuccessfully()
}
[Fact]
- public async Task DeleteAsync_WhenRequestFails_ShouldThrowRequestFailedException()
+ public async Task DeleteAsync_WhenRequestFails_ShouldThrowInvalidOperationException()
{
// Arrange
var blobName = "document-to-delete.pdf";
@@ -241,7 +243,8 @@ public async Task DeleteAsync_WhenRequestFails_ShouldThrowRequestFailedException
var act = () => _service.DeleteAsync(blobName);
// Assert
- await act.Should().ThrowAsync();
+ var exception = await act.Should().ThrowAsync();
+ exception.And.InnerException.Should().BeOfType();
}
[Fact]
diff --git a/src/Modules/Documents/Tests/packages.lock.json b/src/Modules/Documents/Tests/packages.lock.json
index 718e75acb..bcc6f34c3 100644
--- a/src/Modules/Documents/Tests/packages.lock.json
+++ b/src/Modules/Documents/Tests/packages.lock.json
@@ -1263,6 +1263,7 @@
"type": "Project",
"dependencies": {
"MeAjudaAi.Modules.Providers.Domain": "[1.0.0, )",
+ "MeAjudaAi.Modules.ServiceCatalogs.Application": "[1.0.0, )",
"MeAjudaAi.Shared": "[1.0.0, )"
}
},
diff --git a/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/CreateAllowedCity.bru b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/CreateAllowedCity.bru
new file mode 100644
index 000000000..468b87678
--- /dev/null
+++ b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/CreateAllowedCity.bru
@@ -0,0 +1,76 @@
+meta {
+ name: Create Allowed City
+ type: http
+ seq: 3
+}
+
+post {
+ url: {{baseUrl}}/api/v1/admin/allowed-cities
+ body: json
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{accessToken}}
+}
+
+headers {
+ Content-Type: application/json
+}
+
+body:json {
+ {
+ "cityName": "Muriaé",
+ "stateSigla": "MG",
+ "ibgeCode": "3143906"
+ }
+}
+
+docs {
+ # Create Allowed City
+
+ Cria uma nova cidade permitida para restrição geográfica.
+
+ **Autorização**: Requer role `Admin`
+
+ ## Request Body
+
+ ```json
+ {
+ "cityName": "string (required, max 100 chars)",
+ "stateSigla": "string (required, 2 chars uppercase)",
+ "ibgeCode": "string (optional, 7 digits)"
+ }
+ ```
+
+ ### Validações
+
+ - `cityName`: Obrigatório, máximo 100 caracteres
+ - `stateSigla`: Obrigatório, exatamente 2 caracteres maiúsculos (ex: MG, SP, RJ)
+ - `ibgeCode`: Opcional, 7 dígitos se fornecido
+ - Cidade não pode ser duplicada (mesmo nome + estado)
+
+ ## Resposta Sucesso (201 Created)
+
+ ```json
+ {
+ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
+ }
+ ```
+
+ Header `Location`: `/api/v1/admin/allowed-cities/{id}`
+
+ ## Possíveis Erros
+
+ - `400 Bad Request`: Validação falhou ou cidade duplicada
+ ```json
+ {
+ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
+ "title": "Bad Request",
+ "status": 400,
+ "detail": "Cidade 'Muriaé-MG' já cadastrada"
+ }
+ ```
+ - `401 Unauthorized`: Token inválido ou expirado
+ - `403 Forbidden`: Usuário não possui role Admin
+}
diff --git a/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/DeleteAllowedCity.bru b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/DeleteAllowedCity.bru
new file mode 100644
index 000000000..81b3cdffc
--- /dev/null
+++ b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/DeleteAllowedCity.bru
@@ -0,0 +1,65 @@
+meta {
+ name: Delete Allowed City
+ type: http
+ seq: 5
+}
+
+delete {
+ url: {{baseUrl}}/api/v1/locations/admin/allowed-cities/{{allowedCityId}}
+ body: none
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{accessToken}}
+}
+
+headers {
+ Content-Type: application/json
+}
+
+vars:pre-request {
+ allowedCityId: 3fa85f64-5717-4562-b3fc-2c963f66afa6
+}
+
+docs {
+ # Delete Allowed City
+
+ Remove uma cidade permitida (soft delete).
+
+ **Autorização**: Requer role `Admin`
+
+ ## Path Parameters
+
+ - `allowedCityId` (guid): ID da cidade permitida a ser removida
+
+ ## Comportamento
+
+ - Realiza **soft delete** (marca como deletada, não remove fisicamente)
+ - Cidade deletada não aparece mais nas listagens (exceto se filtro incluir deletadas)
+ - Operação é irreversível via API (requer acesso direto ao banco para restaurar)
+
+ ## Resposta Sucesso (204 No Content)
+
+ Sem corpo de resposta.
+
+ ## Possíveis Erros
+
+ - `401 Unauthorized`: Token inválido ou expirado
+ - `403 Forbidden`: Usuário não possui role Admin
+ - `404 Not Found`: Cidade permitida não encontrada
+ ```json
+ {
+ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
+ "title": "Not Found",
+ "status": 404,
+ "detail": "Cidade permitida com allowedCityId '3fa85f64-5717-4562-b3fc-2c963f66afa6' não encontrada"
+ }
+ ```
+
+ ## Notas de Segurança
+
+ - Esta operação requer role `Admin`
+ - Logs de auditoria registram qual usuário realizou a exclusão
+ - Para restaurar uma cidade deletada, contate o DBA
+}
diff --git a/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/GetAllAllowedCities.bru b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/GetAllAllowedCities.bru
new file mode 100644
index 000000000..3e6125b53
--- /dev/null
+++ b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/GetAllAllowedCities.bru
@@ -0,0 +1,56 @@
+meta {
+ name: Get All Allowed Cities
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{baseUrl}}/api/v1/locations/admin/allowed-cities?onlyActive=true
+ body: none
+ auth: bearer
+}
+
+params:query {
+ onlyActive: true
+}
+
+auth:bearer {
+ token: {{accessToken}}
+}
+
+headers {
+ Content-Type: application/json
+}
+
+docs {
+ # Get All Allowed Cities
+
+ Lista todas as cidades permitidas para restrição geográfica.
+
+ **Autorização**: Requer role `Admin`
+
+ ## Query Parameters
+
+ - `onlyActive` (boolean, opcional): Se `true`, retorna apenas cidades ativas. Default: `false`
+
+ ## Resposta Sucesso (200 OK)
+
+ ```json
+ [
+ {
+ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "cityName": "Muriaé",
+ "stateSigla": "MG",
+ "ibgeCode": "3143906",
+ "isActive": true,
+ "createdAt": "2025-12-11T00:00:00Z",
+ "createdBy": "admin@meajudaai.com"
+ }
+ ]
+ ```
+
+ ## Possíveis Erros
+
+ - `401 Unauthorized`: Token inválido ou expirado
+ - `403 Forbidden`: Usuário não possui role Admin
+}
diff --git a/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/GetAllowedCityById.bru b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/GetAllowedCityById.bru
new file mode 100644
index 000000000..f8bb73515
--- /dev/null
+++ b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/GetAllowedCityById.bru
@@ -0,0 +1,57 @@
+meta {
+ name: Get Allowed City By Id
+ type: http
+ seq: 2
+}
+
+get {
+ url: {{baseUrl}}/api/v1/locations/admin/allowed-cities/{{allowedCityId}}
+ body: none
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{accessToken}}
+}
+
+headers {
+ Content-Type: application/json
+}
+
+vars:pre-request {
+ allowedCityId: 3fa85f64-5717-4562-b3fc-2c963f66afa6
+}
+
+docs {
+ # Get Allowed City By Id
+
+ Busca uma cidade permitida específica por ID.
+
+ **Autorização**: Requer role `Admin`
+
+ ## Path Parameters
+
+ - `id` (guid): ID da cidade permitida
+
+ ## Resposta Sucesso (200 OK)
+
+ ```json
+ {
+ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "cityName": "Muriaé",
+ "stateSigla": "MG",
+ "ibgeCode": "3143906",
+ "isActive": true,
+ "createdAt": "2025-12-11T00:00:00Z",
+ "createdBy": "admin@meajudaai.com",
+ "updatedAt": "2025-12-11T12:00:00Z",
+ "updatedBy": "admin@meajudaai.com"
+ }
+ ```
+
+ ## Possíveis Erros
+
+ - `401 Unauthorized`: Token inválido ou expirado
+ - `403 Forbidden`: Usuário não possui role Admin
+ - `404 Not Found`: Cidade permitida não encontrada
+}
diff --git a/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/README.md b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/README.md
new file mode 100644
index 000000000..fcea78e16
--- /dev/null
+++ b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/README.md
@@ -0,0 +1,160 @@
+# AllowedCities Admin API - Bruno Collections
+
+Coleção de requests Bruno para testar os endpoints Admin de gerenciamento de cidades permitidas (Geographic Restrictions).
+
+## 📋 Endpoints Disponíveis
+
+| Request | Método | Endpoint | Descrição |
+|---------|--------|----------|-----------|
+| Get All Allowed Cities | GET | `/api/v1/locations/admin/allowed-cities` | Lista todas as cidades permitidas |
+| Get Allowed City By Id | GET | `/api/v1/locations/admin/allowed-cities/{id}` | Busca cidade específica por ID |
+| Create Allowed City | POST | `/api/v1/locations/admin/allowed-cities` | Cria nova cidade permitida |
+| Update Allowed City | PUT | `/api/v1/locations/admin/allowed-cities/{id}` | Atualiza cidade existente |
+| Delete Allowed City | DELETE | `/api/v1/locations/admin/allowed-cities/{id}` | Remove cidade (soft delete) |
+
+## 🔐 Autenticação
+
+Todos os endpoints requerem:
+- **Bearer Token** válido (JWT)
+- **Role**: `Admin`
+
+### Obter Token
+
+Use a collection `Setup/SetupGetKeycloakToken.bru` para obter um token de admin:
+
+```json
+POST {{keycloakUrl}}/realms/{{realmName}}/protocol/openid-connect/token
+Body:
+{
+ "grant_type": "password",
+ "client_id": "meajudaai-api",
+ "username": "admin@meajudaai.com",
+ "password": "admin123"
+}
+```
+
+Copie o `access_token` e configure na variável `{{accessToken}}`.
+
+## 🌐 Variáveis de Ambiente
+
+Configure as seguintes variáveis no Bruno:
+
+### Development (Local)
+```
+baseUrl = http://localhost:5000
+keycloakUrl = http://localhost:8080
+realmName = meajudaai
+accessToken =
+```
+
+### Staging/Production
+```
+baseUrl = https://api-staging.meajudaai.com
+keycloakUrl = https://auth-staging.meajudaai.com
+realmName = meajudaai
+accessToken =
+```
+
+## 🧪 Fluxo de Teste Sugerido
+
+### 1. Setup Inicial
+```bash
+# Iniciar aplicação localmente
+dotnet run --project src/Aspire/MeAjudaAi.AppHost
+
+# Aguardar API estar disponível
+curl http://localhost:5000/health
+```
+
+### 2. Autenticação
+- Execute `Setup/SetupGetKeycloakToken.bru`
+- Copie o `access_token` retornado
+- Configure variável `{{accessToken}}` no Bruno
+
+### 3. Testes CRUD Completo
+
+#### a) Criar Cidade
+- Execute `CreateAllowedCity.bru`
+- Body exemplo:
+ ```json
+ {
+ "cityName": "São Paulo",
+ "stateSigla": "SP",
+ "ibgeCode": "3550308"
+ }
+ ```
+- Copie o `id` retornado → configure `{{allowedCityId}}`
+
+#### b) Buscar por ID
+- Execute `GetAllowedCityById.bru`
+- Valide: cidade retornada com dados corretos
+
+#### c) Listar Todas
+- Execute `GetAllAllowedCities.bru`
+- Valide: cidade criada aparece na lista
+
+#### d) Atualizar
+- Execute `UpdateAllowedCity.bru`
+- Body exemplo:
+ ```json
+ {
+ "cityName": "São Paulo",
+ "stateSigla": "SP",
+ "ibgeCode": "3550308",
+ "isActive": false
+ }
+ ```
+- Valide: 204 No Content
+
+#### e) Deletar
+- Execute `DeleteAllowedCity.bru`
+- Valide: 204 No Content
+- Execute `GetAllowedCityById.bru` novamente → deve retornar 404
+
+## ✅ Validações de Status Codes
+
+| Cenário | Método | Esperado |
+|---------|--------|----------|
+| Listar cidades (sucesso) | GET | 200 OK |
+| Buscar cidade existente | GET | 200 OK |
+| Buscar cidade inexistente | GET | 404 Not Found |
+| Criar cidade válida | POST | 201 Created |
+| Criar cidade duplicada | POST | 400 Bad Request |
+| Atualizar cidade existente | PUT | 204 No Content |
+| Atualizar cidade inexistente | PUT | 404 Not Found |
+| Atualizar com duplicação | PUT | 400 Bad Request |
+| Deletar cidade existente | DELETE | 204 No Content |
+| Deletar cidade inexistente | DELETE | 404 Not Found |
+| Qualquer operação sem token | ANY | 401 Unauthorized |
+| Qualquer operação sem role Admin | ANY | 403 Forbidden |
+
+## 🐛 Troubleshooting
+
+### 401 Unauthorized
+- Verifique se `{{accessToken}}` está configurado
+- Token pode ter expirado (validade: 5 minutos) → obter novo token
+
+### 403 Forbidden
+- Usuário não possui role `Admin`
+- Use credenciais de admin no Keycloak
+
+### 404 Not Found
+- Verifique se `{{allowedCityId}}` está correto
+- Cidade pode ter sido deletada
+
+### 400 Bad Request - Cidade Duplicada
+- Já existe cidade com mesmo `cityName` + `stateSigla`
+- Use nomes diferentes ou DELETE a cidade existente primeiro
+
+## 📚 Recursos Adicionais
+
+- **Swagger UI**: `http://localhost:5000/swagger`
+- **Architecture Docs**: `docs/architecture.md`
+- **API Spec**: `api/api-spec.json`
+- **E2E Tests**: `tests/MeAjudaAi.E2E.Tests/Modules/Locations/AllowedCitiesEndToEndTests.cs`
+
+## 🔗 Links Relacionados
+
+- [Locations Module Documentation](../../../../docs/modules/locations.md)
+- [Geographic Restriction Architecture](../../../../docs/architecture.md#geographic-restriction)
+- [Sprint 3 Parte 2 Roadmap](../../../../docs/roadmap.md#sprint-3-parte-2)
diff --git a/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/UpdateAllowedCity.bru b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/UpdateAllowedCity.bru
new file mode 100644
index 000000000..472dcea51
--- /dev/null
+++ b/src/Modules/Locations/API/API.Client/AllowedCitiesAdmin/UpdateAllowedCity.bru
@@ -0,0 +1,90 @@
+meta {
+ name: Update Allowed City
+ type: http
+ seq: 4
+}
+
+put {
+ url: {{baseUrl}}/api/v1/locations/admin/allowed-cities/{{allowedCityId}}
+ body: json
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{accessToken}}
+}
+
+headers {
+ Content-Type: application/json
+}
+
+body:json {
+ {
+ "cityName": "Muriaé",
+ "stateSigla": "MG",
+ "ibgeCode": "3143906",
+ "isActive": true
+ }
+}
+
+vars:pre-request {
+ allowedCityId: 3fa85f64-5717-4562-b3fc-2c963f66afa6
+}
+
+docs {
+ # Update Allowed City
+
+ Atualiza uma cidade permitida existente.
+
+ **Autorização**: Requer role `Admin`
+
+ ## Path Parameters
+
+ - `id` (guid): ID da cidade permitida a ser atualizada
+
+ ## Request Body
+
+ ```json
+ {
+ "cityName": "string (required, max 100 chars)",
+ "stateSigla": "string (required, 2 chars uppercase)",
+ "ibgeCode": "string (optional, 7 digits)",
+ "isActive": "boolean (optional, default true)"
+ }
+ ```
+
+ ### Validações
+
+ - `cityName`: Obrigatório, máximo 100 caracteres
+ - `stateSigla`: Obrigatório, exatamente 2 caracteres maiúsculos
+ - `ibgeCode`: Opcional, 7 dígitos se fornecido
+ - `isActive`: Opcional, default `true`
+ - Novo nome/estado não pode duplicar outra cidade existente
+
+ ## Resposta Sucesso (204 No Content)
+
+ Sem corpo de resposta.
+
+ ## Possíveis Erros
+
+ - `400 Bad Request`: Validação falhou ou cidade duplicada
+ ```json
+ {
+ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
+ "title": "Bad Request",
+ "status": 400,
+ "detail": "Cidade 'Muriaé-MG' já cadastrada"
+ }
+ ```
+ - `401 Unauthorized`: Token inválido ou expirado
+ - `403 Forbidden`: Usuário não possui role Admin
+ - `404 Not Found`: Cidade permitida não encontrada
+ ```json
+ {
+ "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
+ "title": "Not Found",
+ "status": 404,
+ "detail": "Cidade permitida com ID '3fa85f64-5717-4562-b3fc-2c963f66afa6' não encontrada"
+ }
+ ```
+}
diff --git a/src/Modules/Locations/Application/Commands/CreateAllowedCityCommand.cs b/src/Modules/Locations/Application/Commands/CreateAllowedCityCommand.cs
new file mode 100644
index 000000000..5c76d51ec
--- /dev/null
+++ b/src/Modules/Locations/Application/Commands/CreateAllowedCityCommand.cs
@@ -0,0 +1,12 @@
+using MeAjudaAi.Shared.Commands;
+
+namespace MeAjudaAi.Modules.Locations.Application.Commands;
+
+///
+/// Command para criar nova cidade permitida
+///
+public sealed record CreateAllowedCityCommand(
+ string CityName,
+ string StateSigla,
+ int? IbgeCode,
+ bool IsActive = true) : Command;
diff --git a/src/Modules/Locations/Application/Commands/DeleteAllowedCityCommand.cs b/src/Modules/Locations/Application/Commands/DeleteAllowedCityCommand.cs
new file mode 100644
index 000000000..14a0981a8
--- /dev/null
+++ b/src/Modules/Locations/Application/Commands/DeleteAllowedCityCommand.cs
@@ -0,0 +1,11 @@
+using MeAjudaAi.Shared.Commands;
+
+namespace MeAjudaAi.Modules.Locations.Application.Commands;
+
+///
+/// Comando para deletar uma cidade permitida.
+///
+public sealed record DeleteAllowedCityCommand : Command
+{
+ public Guid Id { get; init; }
+}
diff --git a/src/Modules/Locations/Application/Commands/UpdateAllowedCityCommand.cs b/src/Modules/Locations/Application/Commands/UpdateAllowedCityCommand.cs
new file mode 100644
index 000000000..9e5724ad6
--- /dev/null
+++ b/src/Modules/Locations/Application/Commands/UpdateAllowedCityCommand.cs
@@ -0,0 +1,15 @@
+using MeAjudaAi.Shared.Commands;
+
+namespace MeAjudaAi.Modules.Locations.Application.Commands;
+
+///
+/// Comando para atualizar uma cidade permitida existente.
+///
+public sealed record UpdateAllowedCityCommand : Command
+{
+ public Guid Id { get; init; }
+ public string CityName { get; init; } = string.Empty;
+ public string StateSigla { get; init; } = string.Empty;
+ public int? IbgeCode { get; init; }
+ public bool IsActive { get; init; }
+}
diff --git a/src/Modules/Locations/Application/DTOs/AllowedCityDto.cs b/src/Modules/Locations/Application/DTOs/AllowedCityDto.cs
new file mode 100644
index 000000000..4bb958829
--- /dev/null
+++ b/src/Modules/Locations/Application/DTOs/AllowedCityDto.cs
@@ -0,0 +1,15 @@
+namespace MeAjudaAi.Modules.Locations.Application.DTOs;
+
+///
+/// DTO para resposta de cidade permitida
+///
+public sealed record AllowedCityDto(
+ Guid Id,
+ string CityName,
+ string StateSigla,
+ int? IbgeCode,
+ bool IsActive,
+ DateTime CreatedAt,
+ DateTime? UpdatedAt,
+ string CreatedBy,
+ string? UpdatedBy);
diff --git a/src/Modules/Locations/Application/Handlers/CreateAllowedCityHandler.cs b/src/Modules/Locations/Application/Handlers/CreateAllowedCityHandler.cs
new file mode 100644
index 000000000..3557732b6
--- /dev/null
+++ b/src/Modules/Locations/Application/Handlers/CreateAllowedCityHandler.cs
@@ -0,0 +1,43 @@
+using MeAjudaAi.Modules.Locations.Application.Commands;
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using MeAjudaAi.Modules.Locations.Domain.Exceptions;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using MeAjudaAi.Shared.Commands;
+using Microsoft.AspNetCore.Http;
+using System.Security.Claims;
+
+namespace MeAjudaAi.Modules.Locations.Application.Handlers;
+
+///
+/// Handler responsável por processar o comando de criação de cidade permitida.
+///
+public sealed class CreateAllowedCityHandler(
+ IAllowedCityRepository repository,
+ IHttpContextAccessor httpContextAccessor) : ICommandHandler
+{
+ public async Task HandleAsync(CreateAllowedCityCommand command, CancellationToken cancellationToken = default)
+ {
+ // Validar se já existe cidade com mesmo nome e estado
+ var exists = await repository.ExistsAsync(command.CityName, command.StateSigla, cancellationToken);
+ if (exists)
+ {
+ throw new DuplicateAllowedCityException(command.CityName, command.StateSigla);
+ }
+
+ // Obter usuário atual (Admin)
+ var currentUser = httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.Email) ?? "system";
+
+ // Criar entidade
+ var allowedCity = new AllowedCity(
+ command.CityName,
+ command.StateSigla,
+ currentUser,
+ command.IbgeCode,
+ command.IsActive);
+
+ // Persistir
+ await repository.AddAsync(allowedCity, cancellationToken);
+
+ return allowedCity.Id;
+ }
+}
diff --git a/src/Modules/Locations/Application/Handlers/DeleteAllowedCityHandler.cs b/src/Modules/Locations/Application/Handlers/DeleteAllowedCityHandler.cs
new file mode 100644
index 000000000..968a06ede
--- /dev/null
+++ b/src/Modules/Locations/Application/Handlers/DeleteAllowedCityHandler.cs
@@ -0,0 +1,22 @@
+using MeAjudaAi.Modules.Locations.Application.Commands;
+using MeAjudaAi.Modules.Locations.Domain.Exceptions;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using MeAjudaAi.Shared.Commands;
+
+namespace MeAjudaAi.Modules.Locations.Application.Handlers;
+
+///
+/// Handler responsável por processar o comando de exclusão de cidade permitida.
+///
+public sealed class DeleteAllowedCityHandler(IAllowedCityRepository repository) : ICommandHandler
+{
+ public async Task HandleAsync(DeleteAllowedCityCommand command, CancellationToken cancellationToken = default)
+ {
+ // Buscar entidade existente
+ var city = await repository.GetByIdAsync(command.Id, cancellationToken)
+ ?? throw new AllowedCityNotFoundException(command.Id);
+
+ // Deletar
+ await repository.DeleteAsync(city, cancellationToken);
+ }
+}
diff --git a/src/Modules/Locations/Application/Handlers/GetAllAllowedCitiesHandler.cs b/src/Modules/Locations/Application/Handlers/GetAllAllowedCitiesHandler.cs
new file mode 100644
index 000000000..933c2c6f8
--- /dev/null
+++ b/src/Modules/Locations/Application/Handlers/GetAllAllowedCitiesHandler.cs
@@ -0,0 +1,32 @@
+using MeAjudaAi.Modules.Locations.Application.DTOs;
+using MeAjudaAi.Modules.Locations.Application.Queries;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using MeAjudaAi.Shared.Queries;
+
+namespace MeAjudaAi.Modules.Locations.Application.Handlers;
+
+///
+/// Handler responsável por processar a query de listagem de cidades permitidas.
+///
+public sealed class GetAllAllowedCitiesHandler(IAllowedCityRepository repository)
+ : IQueryHandler>
+{
+ public async Task> HandleAsync(GetAllAllowedCitiesQuery query, CancellationToken cancellationToken = default)
+ {
+ var cities = query.OnlyActive
+ ? await repository.GetAllActiveAsync(cancellationToken)
+ : await repository.GetAllAsync(cancellationToken);
+
+ return cities.Select(c => new AllowedCityDto(
+ c.Id,
+ c.CityName,
+ c.StateSigla,
+ c.IbgeCode,
+ c.IsActive,
+ c.CreatedAt,
+ c.UpdatedAt,
+ c.CreatedBy,
+ c.UpdatedBy
+ )).ToList();
+ }
+}
diff --git a/src/Modules/Locations/Application/Handlers/GetAllowedCityByIdHandler.cs b/src/Modules/Locations/Application/Handlers/GetAllowedCityByIdHandler.cs
new file mode 100644
index 000000000..9ebf148c0
--- /dev/null
+++ b/src/Modules/Locations/Application/Handlers/GetAllowedCityByIdHandler.cs
@@ -0,0 +1,35 @@
+using MeAjudaAi.Modules.Locations.Application.DTOs;
+using MeAjudaAi.Modules.Locations.Application.Queries;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using MeAjudaAi.Shared.Queries;
+
+namespace MeAjudaAi.Modules.Locations.Application.Handlers;
+
+///
+/// Handler responsável por processar a query de busca de cidade permitida por ID.
+///
+public sealed class GetAllowedCityByIdHandler(IAllowedCityRepository repository)
+ : IQueryHandler
+{
+ public async Task HandleAsync(GetAllowedCityByIdQuery query, CancellationToken cancellationToken = default)
+ {
+ var city = await repository.GetByIdAsync(query.Id, cancellationToken);
+
+ if (city is null)
+ {
+ return null;
+ }
+
+ return new AllowedCityDto(
+ city.Id,
+ city.CityName,
+ city.StateSigla,
+ city.IbgeCode,
+ city.IsActive,
+ city.CreatedAt,
+ city.UpdatedAt,
+ city.CreatedBy,
+ city.UpdatedBy
+ );
+ }
+}
diff --git a/src/Modules/Locations/Application/Handlers/UpdateAllowedCityHandler.cs b/src/Modules/Locations/Application/Handlers/UpdateAllowedCityHandler.cs
new file mode 100644
index 000000000..d73f7c028
--- /dev/null
+++ b/src/Modules/Locations/Application/Handlers/UpdateAllowedCityHandler.cs
@@ -0,0 +1,44 @@
+using MeAjudaAi.Modules.Locations.Application.Commands;
+using MeAjudaAi.Modules.Locations.Domain.Exceptions;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using MeAjudaAi.Shared.Commands;
+using Microsoft.AspNetCore.Http;
+using System.Security.Claims;
+
+namespace MeAjudaAi.Modules.Locations.Application.Handlers;
+
+///
+/// Handler responsável por processar o comando de atualização de cidade permitida.
+///
+public sealed class UpdateAllowedCityHandler(
+ IAllowedCityRepository repository,
+ IHttpContextAccessor httpContextAccessor) : ICommandHandler
+{
+ public async Task HandleAsync(UpdateAllowedCityCommand command, CancellationToken cancellationToken = default)
+ {
+ // Buscar entidade existente
+ var city = await repository.GetByIdAsync(command.Id, cancellationToken)
+ ?? throw new AllowedCityNotFoundException(command.Id);
+
+ // Verificar se novo nome/estado já existe (exceto para esta cidade)
+ var existing = await repository.GetByCityAndStateAsync(command.CityName, command.StateSigla, cancellationToken);
+ if (existing is not null && existing.Id != command.Id)
+ {
+ throw new DuplicateAllowedCityException(command.CityName, command.StateSigla);
+ }
+
+ // Obter usuário atual (Admin)
+ var currentUser = httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.Email) ?? "system";
+
+ // Atualizar entidade
+ city.Update(
+ command.CityName,
+ command.StateSigla,
+ command.IbgeCode,
+ command.IsActive,
+ currentUser);
+
+ // Persistir alterações
+ await repository.UpdateAsync(city, cancellationToken);
+ }
+}
diff --git a/src/Modules/Locations/Application/ModuleApi/LocationsModuleApi.cs b/src/Modules/Locations/Application/ModuleApi/LocationsModuleApi.cs
index a5552f092..921f6063f 100644
--- a/src/Modules/Locations/Application/ModuleApi/LocationsModuleApi.cs
+++ b/src/Modules/Locations/Application/ModuleApi/LocationsModuleApi.cs
@@ -39,7 +39,7 @@ public async Task IsAvailableAsync(CancellationToken cancellationToken = d
var testCep = Cep.Create(HealthCheckCep); // Av. Paulista, São Paulo
if (testCep is not null)
{
- var result = await cepLookupService.LookupAsync(testCep, cancellationToken);
+ _ = await cepLookupService.LookupAsync(testCep, cancellationToken);
// Se conseguiu fazer a requisição (mesmo que retorne null), o módulo está disponível
logger.LogDebug("Location module is available and healthy");
return true;
@@ -48,10 +48,10 @@ public async Task IsAvailableAsync(CancellationToken cancellationToken = d
logger.LogWarning("Location module unavailable - basic operations test failed");
return false;
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException ex)
{
- logger.LogDebug("Location module availability check was cancelled");
- throw;
+ logger.LogDebug(ex, "Location module availability check was cancelled");
+ throw new InvalidOperationException("Location module availability check was cancelled", ex);
}
catch (Exception ex)
{
diff --git a/src/Modules/Locations/Application/Queries/GetAllAllowedCitiesQuery.cs b/src/Modules/Locations/Application/Queries/GetAllAllowedCitiesQuery.cs
new file mode 100644
index 000000000..7f446e9ec
--- /dev/null
+++ b/src/Modules/Locations/Application/Queries/GetAllAllowedCitiesQuery.cs
@@ -0,0 +1,12 @@
+using MeAjudaAi.Modules.Locations.Application.DTOs;
+using MeAjudaAi.Shared.Queries;
+
+namespace MeAjudaAi.Modules.Locations.Application.Queries;
+
+///
+/// Query para obter todas as cidades permitidas.
+///
+public sealed record GetAllAllowedCitiesQuery : Query>
+{
+ public bool OnlyActive { get; init; } = true;
+}
diff --git a/src/Modules/Locations/Application/Queries/GetAllowedCityByIdQuery.cs b/src/Modules/Locations/Application/Queries/GetAllowedCityByIdQuery.cs
new file mode 100644
index 000000000..ada778e5c
--- /dev/null
+++ b/src/Modules/Locations/Application/Queries/GetAllowedCityByIdQuery.cs
@@ -0,0 +1,12 @@
+using MeAjudaAi.Modules.Locations.Application.DTOs;
+using MeAjudaAi.Shared.Queries;
+
+namespace MeAjudaAi.Modules.Locations.Application.Queries;
+
+///
+/// Query para obter uma cidade permitida por ID.
+///
+public sealed record GetAllowedCityByIdQuery : Query
+{
+ public Guid Id { get; init; }
+}
diff --git a/src/Modules/Locations/Application/Services/IIbgeService.cs b/src/Modules/Locations/Application/Services/IIbgeService.cs
index c074e6a87..76df8f378 100644
--- a/src/Modules/Locations/Application/Services/IIbgeService.cs
+++ b/src/Modules/Locations/Application/Services/IIbgeService.cs
@@ -12,13 +12,11 @@ public interface IIbgeService
///
/// Nome da cidade (case-insensitive, aceita acentos)
/// Sigla do estado (opcional, ex: "MG", "RJ", "ES")
- /// Lista de cidades permitidas
/// Token de cancelamento
/// True se a cidade está permitida, False caso contrário
Task ValidateCityInAllowedRegionsAsync(
string cityName,
string? stateSigla,
- IReadOnlyCollection allowedCities,
CancellationToken cancellationToken = default);
///
diff --git a/src/Modules/Locations/Domain/Entities/AllowedCity.cs b/src/Modules/Locations/Domain/Entities/AllowedCity.cs
new file mode 100644
index 000000000..ddecd92ba
--- /dev/null
+++ b/src/Modules/Locations/Domain/Entities/AllowedCity.cs
@@ -0,0 +1,136 @@
+namespace MeAjudaAi.Modules.Locations.Domain.Entities;
+
+///
+/// Entidade que representa uma cidade permitida para operação de prestadores.
+/// Usado para validação geográfica centralizada via banco de dados.
+///
+public sealed class AllowedCity
+{
+ ///
+ /// Identificador único da cidade permitida
+ ///
+ public Guid Id { get; private set; }
+
+ ///
+ /// Nome da cidade (ex: "Muriaé", "Itaperuna")
+ ///
+ public string CityName { get; private set; } = string.Empty;
+
+ ///
+ /// Sigla do estado (ex: "MG", "RJ", "ES")
+ ///
+ public string StateSigla { get; private set; } = string.Empty;
+
+ ///
+ /// Código IBGE do município (opcional - preenchido via integração IBGE)
+ ///
+ public int? IbgeCode { get; private set; }
+
+ ///
+ /// Indica se a cidade está ativa para operação
+ ///
+ public bool IsActive { get; private set; }
+
+ ///
+ /// Data de criação do registro
+ ///
+ public DateTime CreatedAt { get; private set; }
+
+ ///
+ /// Data da última atualização
+ ///
+ public DateTime? UpdatedAt { get; private set; }
+
+ ///
+ /// Usuário que criou o registro (Admin)
+ ///
+ public string CreatedBy { get; private set; } = string.Empty;
+
+ ///
+ /// Usuário que fez a última atualização
+ ///
+ public string? UpdatedBy { get; private set; }
+
+ // EF Core constructor
+ private AllowedCity() { }
+
+ public AllowedCity(
+ string cityName,
+ string stateSigla,
+ string createdBy,
+ int? ibgeCode = null,
+ bool isActive = true)
+ {
+ // Trim first
+ cityName = cityName?.Trim() ?? string.Empty;
+ stateSigla = stateSigla?.Trim().ToUpperInvariant() ?? string.Empty;
+ createdBy = createdBy?.Trim() ?? string.Empty;
+
+ if (string.IsNullOrWhiteSpace(cityName))
+ throw new ArgumentException("Nome da cidade não pode ser vazio", nameof(cityName));
+
+ if (string.IsNullOrWhiteSpace(stateSigla))
+ throw new ArgumentException("Sigla do estado não pode ser vazia", nameof(stateSigla));
+
+ if (stateSigla.Length != 2)
+ throw new ArgumentException("Sigla do estado deve ter 2 caracteres", nameof(stateSigla));
+
+ if (string.IsNullOrWhiteSpace(createdBy))
+ throw new ArgumentException("CreatedBy não pode ser vazio", nameof(createdBy));
+
+ Id = Guid.NewGuid();
+ CityName = cityName;
+ StateSigla = stateSigla;
+ IbgeCode = ibgeCode;
+ IsActive = isActive;
+ CreatedAt = DateTime.UtcNow;
+ CreatedBy = createdBy;
+ }
+
+ public void Update(string cityName, string stateSigla, int? ibgeCode, bool isActive, string updatedBy)
+ {
+ // Trim first
+ cityName = cityName?.Trim() ?? string.Empty;
+ stateSigla = stateSigla?.Trim().ToUpperInvariant() ?? string.Empty;
+ updatedBy = updatedBy?.Trim() ?? string.Empty;
+
+ if (string.IsNullOrWhiteSpace(cityName))
+ throw new ArgumentException("Nome da cidade não pode ser vazio", nameof(cityName));
+
+ if (string.IsNullOrWhiteSpace(stateSigla))
+ throw new ArgumentException("Sigla do estado não pode ser vazia", nameof(stateSigla));
+
+ if (stateSigla.Length != 2)
+ throw new ArgumentException("Sigla do estado deve ter 2 caracteres", nameof(stateSigla));
+
+ if (string.IsNullOrWhiteSpace(updatedBy))
+ throw new ArgumentException("UpdatedBy não pode ser vazio", nameof(updatedBy));
+
+ CityName = cityName;
+ StateSigla = stateSigla;
+ IbgeCode = ibgeCode;
+ IsActive = isActive;
+ UpdatedAt = DateTime.UtcNow;
+ UpdatedBy = updatedBy;
+ }
+
+ public void Activate(string updatedBy)
+ {
+ if (string.IsNullOrWhiteSpace(updatedBy))
+ throw new ArgumentException("UpdatedBy não pode ser vazio", nameof(updatedBy));
+
+ IsActive = true;
+ UpdatedAt = DateTime.UtcNow;
+ UpdatedBy = updatedBy;
+ }
+
+ public void Deactivate(string updatedBy)
+ {
+ if (string.IsNullOrWhiteSpace(updatedBy))
+ throw new ArgumentException("UpdatedBy não pode ser vazio", nameof(updatedBy));
+
+ IsActive = false;
+ UpdatedAt = DateTime.UtcNow;
+ UpdatedBy = updatedBy;
+ }
+}
diff --git a/src/Modules/Locations/Domain/Exceptions/AllowedCityNotFoundException.cs b/src/Modules/Locations/Domain/Exceptions/AllowedCityNotFoundException.cs
new file mode 100644
index 000000000..5365a7f8f
--- /dev/null
+++ b/src/Modules/Locations/Domain/Exceptions/AllowedCityNotFoundException.cs
@@ -0,0 +1,10 @@
+namespace MeAjudaAi.Modules.Locations.Domain.Exceptions;
+
+///
+/// Exceção lançada quando uma cidade permitida não é encontrada.
+///
+public sealed class AllowedCityNotFoundException(Guid cityId)
+ : NotFoundException($"Cidade permitida com ID '{cityId}' não encontrada")
+{
+ public Guid CityId { get; } = cityId;
+}
diff --git a/src/Modules/Locations/Domain/Exceptions/BadRequestException.cs b/src/Modules/Locations/Domain/Exceptions/BadRequestException.cs
new file mode 100644
index 000000000..751b23d1e
--- /dev/null
+++ b/src/Modules/Locations/Domain/Exceptions/BadRequestException.cs
@@ -0,0 +1,8 @@
+namespace MeAjudaAi.Modules.Locations.Domain.Exceptions;
+
+///
+/// Exceção base para requisições inválidas (400 Bad Request).
+///
+public abstract class BadRequestException(string message) : Exception(message)
+{
+}
diff --git a/src/Modules/Locations/Domain/Exceptions/DuplicateAllowedCityException.cs b/src/Modules/Locations/Domain/Exceptions/DuplicateAllowedCityException.cs
new file mode 100644
index 000000000..8b6d20cff
--- /dev/null
+++ b/src/Modules/Locations/Domain/Exceptions/DuplicateAllowedCityException.cs
@@ -0,0 +1,11 @@
+namespace MeAjudaAi.Modules.Locations.Domain.Exceptions;
+
+///
+/// Exceção lançada quando já existe uma cidade permitida com mesmo nome e estado.
+///
+public sealed class DuplicateAllowedCityException(string cityName, string stateSigla)
+ : BadRequestException($"Cidade '{cityName}-{stateSigla}' já cadastrada")
+{
+ public string CityName { get; } = cityName;
+ public string StateSigla { get; } = stateSigla;
+}
diff --git a/src/Modules/Locations/Domain/Exceptions/NotFoundException.cs b/src/Modules/Locations/Domain/Exceptions/NotFoundException.cs
new file mode 100644
index 000000000..2d4a462dd
--- /dev/null
+++ b/src/Modules/Locations/Domain/Exceptions/NotFoundException.cs
@@ -0,0 +1,8 @@
+namespace MeAjudaAi.Modules.Locations.Domain.Exceptions;
+
+///
+/// Exceção base para recursos não encontrados (404 Not Found).
+///
+public abstract class NotFoundException(string message) : Exception(message)
+{
+}
diff --git a/src/Modules/Locations/Domain/Repositories/IAllowedCityRepository.cs b/src/Modules/Locations/Domain/Repositories/IAllowedCityRepository.cs
new file mode 100644
index 000000000..fccddbfd3
--- /dev/null
+++ b/src/Modules/Locations/Domain/Repositories/IAllowedCityRepository.cs
@@ -0,0 +1,54 @@
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+
+namespace MeAjudaAi.Modules.Locations.Domain.Repositories;
+
+///
+/// Repositório para gerenciamento de cidades permitidas
+///
+public interface IAllowedCityRepository
+{
+ ///
+ /// Busca todas as cidades permitidas ativas
+ ///
+ Task> GetAllActiveAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Busca todas as cidades permitidas (incluindo inativas)
+ ///
+ Task> GetAllAsync(CancellationToken cancellationToken = default);
+
+ ///
+ /// Busca cidade permitida por ID
+ ///
+ Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
+
+ ///
+ /// Busca cidade permitida por nome e estado
+ ///
+ Task GetByCityAndStateAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
+
+ ///
+ /// Verifica se uma cidade está permitida (ativa)
+ ///
+ Task IsCityAllowedAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
+
+ ///
+ /// Adiciona nova cidade permitida
+ ///
+ Task AddAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
+
+ ///
+ /// Atualiza cidade permitida existente
+ ///
+ Task UpdateAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
+
+ ///
+ /// Remove cidade permitida
+ ///
+ Task DeleteAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default);
+
+ ///
+ /// Verifica se já existe cidade com mesmo nome e estado
+ ///
+ Task ExistsAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default);
+}
diff --git a/src/Modules/Locations/Infrastructure/API/Endpoints/CreateAllowedCityEndpoint.cs b/src/Modules/Locations/Infrastructure/API/Endpoints/CreateAllowedCityEndpoint.cs
new file mode 100644
index 000000000..a1c253127
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/API/Endpoints/CreateAllowedCityEndpoint.cs
@@ -0,0 +1,50 @@
+using MeAjudaAi.Modules.Locations.Application.Commands;
+using MeAjudaAi.Shared.Authorization;
+using MeAjudaAi.Shared.Commands;
+using MeAjudaAi.Shared.Contracts;
+using MeAjudaAi.Shared.Endpoints;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.API.Endpoints;
+
+///
+/// Endpoint para criar nova cidade permitida (Admin only)
+///
+public class CreateAllowedCityEndpoint : BaseEndpoint, IEndpoint
+{
+ public static void Map(IEndpointRouteBuilder app)
+ => app.MapPost("/api/v1/admin/allowed-cities", CreateAsync)
+ .WithName("CreateAllowedCity")
+ .WithSummary("Create new allowed city")
+ .WithDescription("Creates a new allowed city for provider operations (Admin only)")
+ .Produces>(StatusCodes.Status201Created)
+ .Produces(StatusCodes.Status400BadRequest)
+ .RequireAdmin();
+
+ private static async Task CreateAsync(
+ CreateAllowedCityRequest request,
+ ICommandDispatcher commandDispatcher,
+ CancellationToken cancellationToken)
+ {
+ var command = new CreateAllowedCityCommand(
+ request.CityName,
+ request.StateSigla,
+ request.IbgeCode,
+ request.IsActive);
+
+ var cityId = await commandDispatcher.SendAsync(command, cancellationToken);
+
+ return Results.Created($"/api/v1/admin/allowed-cities/{cityId}", new Response(cityId, 201));
+ }
+}
+
+///
+/// Request DTO para criação de cidade permitida
+///
+public sealed record CreateAllowedCityRequest(
+ string CityName,
+ string StateSigla,
+ int? IbgeCode,
+ bool IsActive = true);
diff --git a/src/Modules/Locations/Infrastructure/API/Endpoints/DeleteAllowedCityEndpoint.cs b/src/Modules/Locations/Infrastructure/API/Endpoints/DeleteAllowedCityEndpoint.cs
new file mode 100644
index 000000000..12b91470a
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/API/Endpoints/DeleteAllowedCityEndpoint.cs
@@ -0,0 +1,37 @@
+using MeAjudaAi.Modules.Locations.Application.Commands;
+using MeAjudaAi.Shared.Authorization;
+using MeAjudaAi.Shared.Commands;
+using MeAjudaAi.Shared.Contracts;
+using MeAjudaAi.Shared.Endpoints;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.API.Endpoints;
+
+///
+/// Endpoint para deletar cidade permitida (Admin only)
+///
+public class DeleteAllowedCityEndpoint : BaseEndpoint, IEndpoint
+{
+ public static void Map(IEndpointRouteBuilder app)
+ => app.MapDelete("/api/v1/admin/allowed-cities/{id:guid}", DeleteAsync)
+ .WithName("DeleteAllowedCity")
+ .WithSummary("Delete allowed city")
+ .WithDescription("Deletes an allowed city")
+ .Produces(StatusCodes.Status204NoContent)
+ .Produces(StatusCodes.Status404NotFound)
+ .RequireAdmin();
+
+ private static async Task DeleteAsync(
+ Guid id,
+ ICommandDispatcher commandDispatcher,
+ CancellationToken cancellationToken)
+ {
+ var command = new DeleteAllowedCityCommand { Id = id };
+
+ await commandDispatcher.SendAsync(command, cancellationToken);
+
+ return Results.NoContent();
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/API/Endpoints/GetAllAllowedCitiesEndpoint.cs b/src/Modules/Locations/Infrastructure/API/Endpoints/GetAllAllowedCitiesEndpoint.cs
new file mode 100644
index 000000000..55eba4139
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/API/Endpoints/GetAllAllowedCitiesEndpoint.cs
@@ -0,0 +1,37 @@
+using MeAjudaAi.Modules.Locations.Application.DTOs;
+using MeAjudaAi.Modules.Locations.Application.Queries;
+using MeAjudaAi.Shared.Authorization;
+using MeAjudaAi.Shared.Contracts;
+using MeAjudaAi.Shared.Endpoints;
+using MeAjudaAi.Shared.Queries;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.API.Endpoints;
+
+///
+/// Endpoint para listar todas as cidades permitidas (Admin only)
+///
+public class GetAllAllowedCitiesEndpoint : BaseEndpoint, IEndpoint
+{
+ public static void Map(IEndpointRouteBuilder app)
+ => app.MapGet("/api/v1/admin/allowed-cities", GetAllAsync)
+ .WithName("GetAllAllowedCities")
+ .WithSummary("Get all allowed cities")
+ .WithDescription("Retrieves all allowed cities (optionally only active ones)")
+ .Produces>>(StatusCodes.Status200OK)
+ .RequireAdmin();
+
+ private static async Task GetAllAsync(
+ bool onlyActive = false,
+ IQueryDispatcher queryDispatcher = default!,
+ CancellationToken cancellationToken = default)
+ {
+ var query = new GetAllAllowedCitiesQuery { OnlyActive = onlyActive };
+
+ var result = await queryDispatcher.QueryAsync>(query, cancellationToken);
+
+ return Results.Ok(new Response>(result));
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/API/Endpoints/GetAllowedCityByIdEndpoint.cs b/src/Modules/Locations/Infrastructure/API/Endpoints/GetAllowedCityByIdEndpoint.cs
new file mode 100644
index 000000000..ba9e9c620
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/API/Endpoints/GetAllowedCityByIdEndpoint.cs
@@ -0,0 +1,40 @@
+using MeAjudaAi.Modules.Locations.Application.DTOs;
+using MeAjudaAi.Modules.Locations.Application.Queries;
+using MeAjudaAi.Shared.Authorization;
+using MeAjudaAi.Shared.Contracts;
+using MeAjudaAi.Shared.Endpoints;
+using MeAjudaAi.Shared.Queries;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.API.Endpoints;
+
+///
+/// Endpoint para buscar cidade permitida por ID (Admin only)
+///
+public class GetAllowedCityByIdEndpoint : BaseEndpoint, IEndpoint
+{
+ public static void Map(IEndpointRouteBuilder app)
+ => app.MapGet("/api/v1/admin/allowed-cities/{id:guid}", GetByIdAsync)
+ .WithName("GetAllowedCityById")
+ .WithSummary("Get allowed city by ID")
+ .WithDescription("Retrieves a specific allowed city by its ID")
+ .Produces>(StatusCodes.Status200OK)
+ .Produces(StatusCodes.Status404NotFound)
+ .RequireAdmin();
+
+ private static async Task GetByIdAsync(
+ Guid id,
+ IQueryDispatcher queryDispatcher,
+ CancellationToken cancellationToken)
+ {
+ var query = new GetAllowedCityByIdQuery { Id = id };
+
+ var result = await queryDispatcher.QueryAsync(query, cancellationToken);
+
+ return result is not null
+ ? Results.Ok(new Response(result))
+ : Results.NotFound(new Response(default, 404, "Cidade permitida não encontrada"));
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/API/Endpoints/UpdateAllowedCityEndpoint.cs b/src/Modules/Locations/Infrastructure/API/Endpoints/UpdateAllowedCityEndpoint.cs
new file mode 100644
index 000000000..cc0be3cdc
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/API/Endpoints/UpdateAllowedCityEndpoint.cs
@@ -0,0 +1,55 @@
+using MeAjudaAi.Modules.Locations.Application.Commands;
+using MeAjudaAi.Shared.Authorization;
+using MeAjudaAi.Shared.Commands;
+using MeAjudaAi.Shared.Contracts;
+using MeAjudaAi.Shared.Endpoints;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.API.Endpoints;
+
+///
+/// Endpoint para atualizar cidade permitida existente (Admin only)
+///
+public class UpdateAllowedCityEndpoint : BaseEndpoint, IEndpoint
+{
+ public static void Map(IEndpointRouteBuilder app)
+ => app.MapPut("/api/v1/admin/allowed-cities/{id:guid}", UpdateAsync)
+ .WithName("UpdateAllowedCity")
+ .WithSummary("Update allowed city")
+ .WithDescription("Updates an existing allowed city")
+ .Produces>(StatusCodes.Status200OK)
+ .Produces(StatusCodes.Status404NotFound)
+ .Produces(StatusCodes.Status400BadRequest)
+ .RequireAdmin();
+
+ private static async Task UpdateAsync(
+ Guid id,
+ UpdateAllowedCityRequest request,
+ ICommandDispatcher commandDispatcher,
+ CancellationToken cancellationToken)
+ {
+ var command = new UpdateAllowedCityCommand
+ {
+ Id = id,
+ CityName = request.CityName,
+ StateSigla = request.StateSigla,
+ IbgeCode = request.IbgeCode,
+ IsActive = request.IsActive
+ };
+
+ await commandDispatcher.SendAsync(command, cancellationToken);
+
+ return Results.Ok(new Response("Cidade permitida atualizada com sucesso"));
+ }
+}
+
+///
+/// Request DTO para atualização de cidade permitida
+///
+public sealed record UpdateAllowedCityRequest(
+ string CityName,
+ string StateSigla,
+ int? IbgeCode,
+ bool IsActive);
diff --git a/src/Modules/Locations/Infrastructure/Extensions.cs b/src/Modules/Locations/Infrastructure/Extensions.cs
index 5210cc563..3a591338f 100644
--- a/src/Modules/Locations/Infrastructure/Extensions.cs
+++ b/src/Modules/Locations/Infrastructure/Extensions.cs
@@ -1,11 +1,19 @@
using MeAjudaAi.Modules.Locations.Application.ModuleApi;
using MeAjudaAi.Modules.Locations.Application.Services;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using MeAjudaAi.Modules.Locations.Infrastructure.API.Endpoints;
using MeAjudaAi.Modules.Locations.Infrastructure.ExternalApis.Clients;
using MeAjudaAi.Modules.Locations.Infrastructure.ExternalApis.Clients.Interfaces;
+using MeAjudaAi.Modules.Locations.Infrastructure.Filters;
+using MeAjudaAi.Modules.Locations.Infrastructure.Persistence;
+using MeAjudaAi.Modules.Locations.Infrastructure.Repositories;
using MeAjudaAi.Modules.Locations.Infrastructure.Services;
+using MeAjudaAi.Shared.Commands;
using MeAjudaAi.Shared.Contracts.Modules.Locations;
using MeAjudaAi.Shared.Geolocation;
+using MeAjudaAi.Shared.Queries;
using Microsoft.AspNetCore.Builder;
+using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -21,6 +29,36 @@ public static class Extensions
///
public static IServiceCollection AddLocationModule(this IServiceCollection services, IConfiguration configuration)
{
+ // Registrar DbContext para Locations module
+ services.AddDbContext((serviceProvider, options) =>
+ {
+ var connectionString = configuration.GetConnectionString("DefaultConnection")
+ ?? throw new InvalidOperationException("DefaultConnection não configurada");
+
+ options.UseNpgsql(connectionString,
+ npgsqlOptions =>
+ {
+ npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "locations");
+ npgsqlOptions.MigrationsAssembly("MeAjudaAi.Modules.Locations.Infrastructure");
+ });
+
+ options.EnableDetailedErrors();
+ options.EnableSensitiveDataLogging(configuration.GetValue("Logging:EnableSensitiveDataLogging"));
+ });
+
+ // Registrar Func para uso em migrations (design-time)
+ services.AddScoped>(provider => () =>
+ {
+ var context = provider.GetRequiredService();
+ return context;
+ });
+
+ // Registrar repositórios
+ services.AddScoped();
+
+ // Registrar ExceptionHandler para exceções de domínio
+ services.AddExceptionHandler();
+
// Registrar HTTP clients para APIs de CEP
// ServiceDefaults já configura resiliência (retry, circuit breaker, timeout)
services.AddHttpClient(client =>
@@ -63,7 +101,7 @@ public static IServiceCollection AddLocationModule(this IServiceCollection servi
var baseUrl = configuration["Locations:ExternalApis:IBGE:BaseUrl"]
?? "https://servicodados.ibge.gov.br/api/v1/localidades/"; // Fallback para testes
- if (!baseUrl.EndsWith("/"))
+ if (!baseUrl.EndsWith('/'))
{
baseUrl += "/";
}
@@ -82,17 +120,53 @@ public static IServiceCollection AddLocationModule(this IServiceCollection servi
// Registrar Module API
services.AddScoped();
+ // Registrar Command e Query Handlers automaticamente
+ var applicationAssembly = typeof(Application.Handlers.CreateAllowedCityHandler).Assembly;
+
+ // Registrar todos os ICommandHandler e ICommandHandler
+ var commandHandlerTypes = applicationAssembly.GetTypes()
+ .Where(t => t.IsClass && !t.IsAbstract)
+ .SelectMany(t => t.GetInterfaces()
+ .Where(i => i.IsGenericType &&
+ (i.GetGenericTypeDefinition() == typeof(ICommandHandler<>) ||
+ i.GetGenericTypeDefinition() == typeof(ICommandHandler<,>)))
+ .Select(i => new { Interface = i, Implementation = t }))
+ .ToList();
+
+ foreach (var handler in commandHandlerTypes)
+ {
+ services.AddScoped(handler.Interface, handler.Implementation);
+ }
+
+ // Registrar todos os IQueryHandler
+ var queryHandlerTypes = applicationAssembly.GetTypes()
+ .Where(t => t.IsClass && !t.IsAbstract)
+ .SelectMany(t => t.GetInterfaces()
+ .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IQueryHandler<,>))
+ .Select(i => new { Interface = i, Implementation = t }))
+ .ToList();
+
+ foreach (var handler in queryHandlerTypes)
+ {
+ services.AddScoped(handler.Interface, handler.Implementation);
+ }
+
return services;
}
///
- /// Configura o middleware do módulo Location.
- /// Location module exposes only internal services, no endpoints or middleware.
- /// This method exists for consistency with other modules.
+ /// Configura os endpoints do módulo Location.
+ /// Registra endpoints administrativos para gerenciamento de cidades permitidas.
///
public static WebApplication UseLocationModule(this WebApplication app)
{
- // No middleware or endpoints to configure
+ // Registrar endpoints administrativos (Admin only)
+ CreateAllowedCityEndpoint.Map(app);
+ GetAllAllowedCitiesEndpoint.Map(app);
+ GetAllowedCityByIdEndpoint.Map(app);
+ UpdateAllowedCityEndpoint.Map(app);
+ DeleteAllowedCityEndpoint.Map(app);
+
return app;
}
}
diff --git a/src/Modules/Locations/Infrastructure/ExternalApis/Clients/IbgeClient.cs b/src/Modules/Locations/Infrastructure/ExternalApis/Clients/IbgeClient.cs
index 4cd018a2b..d9f2fd142 100644
--- a/src/Modules/Locations/Infrastructure/ExternalApis/Clients/IbgeClient.cs
+++ b/src/Modules/Locations/Infrastructure/ExternalApis/Clients/IbgeClient.cs
@@ -83,19 +83,25 @@ public sealed class IbgeClient(HttpClient httpClient, ILogger logger
{
// Re-throw HTTP exceptions (500, timeout, etc) to enable middleware fallback
logger.LogError(ex, "HTTP error querying IBGE for municipality {CityName}", cityName);
- throw;
+ throw new InvalidOperationException(
+ $"HTTP error querying IBGE API for municipality '{cityName}' (Status: {ex.StatusCode})",
+ ex);
}
- catch (TaskCanceledException ex)
+ catch (TaskCanceledException ex) when (ex != null)
{
// Re-throw timeout exceptions to enable middleware fallback
logger.LogError(ex, "Timeout querying IBGE for municipality {CityName}", cityName);
- throw;
+ throw new TimeoutException(
+ $"IBGE API request timed out while querying municipality '{cityName}'",
+ ex);
}
catch (Exception ex)
{
// For other exceptions (JSON parsing, etc), re-throw to enable fallback
logger.LogError(ex, "Unexpected error querying IBGE for municipality {CityName}", cityName);
- throw;
+ throw new InvalidOperationException(
+ $"Unexpected error querying IBGE API for municipality '{cityName}' (may be JSON parsing or network issue)",
+ ex);
}
}
@@ -133,11 +139,11 @@ public async Task> GetMunicipiosByUFAsync(string ufSigla, Cancel
///
/// Valida se uma cidade existe na UF especificada.
///
- public async Task ValidateCityInStateAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default)
+ public async Task ValidateCityInStateAsync(string city, string state, CancellationToken cancellationToken = default)
{
try
{
- var municipio = await GetMunicipioByNameAsync(cityName, cancellationToken);
+ var municipio = await GetMunicipioByNameAsync(city, cancellationToken);
if (municipio is null)
{
@@ -145,11 +151,11 @@ public async Task ValidateCityInStateAsync(string cityName, string stateSi
}
var ufSigla = municipio.GetEstadoSigla();
- return string.Equals(ufSigla, stateSigla, StringComparison.OrdinalIgnoreCase);
+ return string.Equals(ufSigla, state, StringComparison.OrdinalIgnoreCase);
}
catch (Exception ex)
{
- logger.LogError(ex, "Erro ao validar cidade {CityName} na UF {UF}", cityName, stateSigla);
+ logger.LogError(ex, "Erro ao validar cidade {CityName} na UF {UF}", city, state);
return false;
}
}
diff --git a/src/Modules/Locations/Infrastructure/Filters/LocationsExceptionFilter.cs b/src/Modules/Locations/Infrastructure/Filters/LocationsExceptionFilter.cs
new file mode 100644
index 000000000..51bc60a31
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Filters/LocationsExceptionFilter.cs
@@ -0,0 +1,42 @@
+using MeAjudaAi.Modules.Locations.Domain.Exceptions;
+using MeAjudaAi.Shared.Contracts;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Filters;
+
+///
+/// Filter que captura exceções de domínio e converte para respostas HTTP adequadas.
+///
+public sealed class LocationsExceptionFilter(ILogger logger) : IExceptionFilter
+{
+ public void OnException(ExceptionContext context)
+ {
+ switch (context.Exception)
+ {
+ case NotFoundException notFoundEx:
+ logger.LogWarning(notFoundEx, "Resource not found: {Message}", notFoundEx.Message);
+ context.Result = new NotFoundObjectResult(new ProblemDetails
+ {
+ Status = StatusCodes.Status404NotFound,
+ Title = "Resource not found",
+ Detail = notFoundEx.Message
+ });
+ context.ExceptionHandled = true;
+ break;
+
+ case BadRequestException badRequestEx:
+ logger.LogWarning(badRequestEx, "Bad request: {Message}", badRequestEx.Message);
+ context.Result = new BadRequestObjectResult(new ProblemDetails
+ {
+ Status = StatusCodes.Status400BadRequest,
+ Title = "Bad request",
+ Detail = badRequestEx.Message
+ });
+ context.ExceptionHandled = true;
+ break;
+ }
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Filters/LocationsExceptionHandler.cs b/src/Modules/Locations/Infrastructure/Filters/LocationsExceptionHandler.cs
new file mode 100644
index 000000000..dbbe37465
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Filters/LocationsExceptionHandler.cs
@@ -0,0 +1,57 @@
+using MeAjudaAi.Modules.Locations.Domain.Exceptions;
+using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Filters;
+
+///
+/// Handler que captura exceções de domínio e converte para respostas HTTP adequadas.
+///
+public sealed class LocationsExceptionHandler(ILogger logger) : IExceptionHandler
+{
+ public async ValueTask TryHandleAsync(
+ HttpContext httpContext,
+ Exception exception,
+ CancellationToken cancellationToken)
+ {
+ ProblemDetails? problemDetails = exception switch
+ {
+ NotFoundException notFoundEx => HandleNotFoundException(notFoundEx),
+ BadRequestException badRequestEx => HandleBadRequestException(badRequestEx),
+ _ => null
+ };
+
+ if (problemDetails is null)
+ {
+ return false;
+ }
+
+ httpContext.Response.StatusCode = problemDetails.Status ?? StatusCodes.Status500InternalServerError;
+ await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
+ return true;
+ }
+
+ private ProblemDetails HandleNotFoundException(NotFoundException exception)
+ {
+ logger.LogWarning(exception, "Resource not found: {Message}", exception.Message);
+ return new ProblemDetails
+ {
+ Status = StatusCodes.Status404NotFound,
+ Title = "Resource not found",
+ Detail = exception.Message
+ };
+ }
+
+ private ProblemDetails HandleBadRequestException(BadRequestException exception)
+ {
+ logger.LogWarning(exception, "Bad request: {Message}", exception.Message);
+ return new ProblemDetails
+ {
+ Status = StatusCodes.Status400BadRequest,
+ Title = "Bad request",
+ Detail = exception.Message
+ };
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Migrations/20251212002108_InitialAllowedCities.Designer.cs b/src/Modules/Locations/Infrastructure/Migrations/20251212002108_InitialAllowedCities.Designer.cs
new file mode 100644
index 000000000..93b9fbb7d
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Migrations/20251212002108_InitialAllowedCities.Designer.cs
@@ -0,0 +1,88 @@
+//
+using System;
+using MeAjudaAi.Modules.Locations.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Migrations
+{
+ [DbContext(typeof(LocationsDbContext))]
+ [Migration("20251212002108_InitialAllowedCities")]
+ partial class InitialAllowedCities
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("locations")
+ .HasAnnotation("ProductVersion", "10.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Locations.Domain.Entities.AllowedCity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CityName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("IbgeCode")
+ .HasColumnType("integer");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("StateSigla")
+ .IsRequired()
+ .HasMaxLength(2)
+ .HasColumnType("character(2)")
+ .IsFixedLength();
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IbgeCode")
+ .IsUnique()
+ .HasDatabaseName("IX_AllowedCities_IbgeCode")
+ .HasFilter("\"IbgeCode\" IS NOT NULL");
+
+ b.HasIndex("IsActive")
+ .HasDatabaseName("IX_AllowedCities_IsActive");
+
+ b.HasIndex("CityName", "StateSigla")
+ .IsUnique()
+ .HasDatabaseName("IX_AllowedCities_CityName_State");
+
+ b.ToTable("allowed_cities", "locations");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Migrations/20251212002108_InitialAllowedCities.cs b/src/Modules/Locations/Infrastructure/Migrations/20251212002108_InitialAllowedCities.cs
new file mode 100644
index 000000000..bc99985ca
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Migrations/20251212002108_InitialAllowedCities.cs
@@ -0,0 +1,67 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Migrations
+{
+ ///
+ public partial class InitialAllowedCities : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.EnsureSchema(
+ name: "locations");
+
+ migrationBuilder.CreateTable(
+ name: "allowed_cities",
+ schema: "locations",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ CityName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ StateSigla = table.Column(type: "character(2)", fixedLength: true, maxLength: 2, nullable: false),
+ IbgeCode = table.Column(type: "integer", nullable: true),
+ IsActive = table.Column(type: "boolean", nullable: false, defaultValue: true),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false),
+ UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ CreatedBy = table.Column(type: "character varying(256)", maxLength: 256, nullable: false),
+ UpdatedBy = table.Column(type: "character varying(256)", maxLength: 256, nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_allowed_cities", x => x.Id);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AllowedCities_CityName_State",
+ schema: "locations",
+ table: "allowed_cities",
+ columns: new[] { "CityName", "StateSigla" },
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AllowedCities_IbgeCode",
+ schema: "locations",
+ table: "allowed_cities",
+ column: "IbgeCode",
+ unique: true,
+ filter: "\"IbgeCode\" IS NOT NULL");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_AllowedCities_IsActive",
+ schema: "locations",
+ table: "allowed_cities",
+ column: "IsActive");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "allowed_cities",
+ schema: "locations");
+ }
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Migrations/LocationsDbContextModelSnapshot.cs b/src/Modules/Locations/Infrastructure/Migrations/LocationsDbContextModelSnapshot.cs
new file mode 100644
index 000000000..05ed6cc8d
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Migrations/LocationsDbContextModelSnapshot.cs
@@ -0,0 +1,85 @@
+//
+using System;
+using MeAjudaAi.Modules.Locations.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Migrations
+{
+ [DbContext(typeof(LocationsDbContext))]
+ partial class LocationsDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("locations")
+ .HasAnnotation("ProductVersion", "10.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MeAjudaAi.Modules.Locations.Domain.Entities.AllowedCity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CityName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("CreatedBy")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.Property("IbgeCode")
+ .HasColumnType("integer");
+
+ b.Property("IsActive")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("StateSigla")
+ .IsRequired()
+ .HasMaxLength(2)
+ .HasColumnType("character(2)")
+ .IsFixedLength();
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("UpdatedBy")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("IbgeCode")
+ .IsUnique()
+ .HasDatabaseName("IX_AllowedCities_IbgeCode")
+ .HasFilter("\"IbgeCode\" IS NOT NULL");
+
+ b.HasIndex("IsActive")
+ .HasDatabaseName("IX_AllowedCities_IsActive");
+
+ b.HasIndex("CityName", "StateSigla")
+ .IsUnique()
+ .HasDatabaseName("IX_AllowedCities_CityName_State");
+
+ b.ToTable("allowed_cities", "locations");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Persistence/Configurations/AllowedCityConfiguration.cs b/src/Modules/Locations/Infrastructure/Persistence/Configurations/AllowedCityConfiguration.cs
new file mode 100644
index 000000000..10cdabb67
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Persistence/Configurations/AllowedCityConfiguration.cs
@@ -0,0 +1,61 @@
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Persistence.Configurations;
+
+///
+/// Entity Framework configuration for AllowedCity entity
+///
+internal sealed class AllowedCityConfiguration : IEntityTypeConfiguration
+{
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("allowed_cities", "locations");
+
+ builder.HasKey(x => x.Id);
+
+ builder.Property(x => x.CityName)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ builder.Property(x => x.StateSigla)
+ .IsRequired()
+ .HasMaxLength(2)
+ .IsFixedLength();
+
+ builder.Property(x => x.IbgeCode)
+ .IsRequired(false);
+
+ builder.Property(x => x.IsActive)
+ .IsRequired()
+ .HasDefaultValue(true);
+
+ builder.Property(x => x.CreatedAt)
+ .IsRequired();
+
+ builder.Property(x => x.UpdatedAt)
+ .IsRequired(false);
+
+ builder.Property(x => x.CreatedBy)
+ .IsRequired()
+ .HasMaxLength(256);
+
+ builder.Property(x => x.UpdatedBy)
+ .IsRequired(false)
+ .HasMaxLength(256);
+
+ // Índices para performance
+ builder.HasIndex(x => new { x.CityName, x.StateSigla })
+ .IsUnique()
+ .HasDatabaseName("IX_AllowedCities_CityName_State");
+
+ builder.HasIndex(x => x.IsActive)
+ .HasDatabaseName("IX_AllowedCities_IsActive");
+
+ builder.HasIndex(x => x.IbgeCode)
+ .IsUnique()
+ .HasFilter("\"IbgeCode\" IS NOT NULL")
+ .HasDatabaseName("IX_AllowedCities_IbgeCode");
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Persistence/LocationsDbContext.cs b/src/Modules/Locations/Infrastructure/Persistence/LocationsDbContext.cs
new file mode 100644
index 000000000..58475c8cb
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Persistence/LocationsDbContext.cs
@@ -0,0 +1,69 @@
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using MeAjudaAi.Shared.Database;
+using MeAjudaAi.Shared.Events;
+using Microsoft.EntityFrameworkCore;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Persistence;
+
+///
+/// Database context for the Locations module.
+/// Manages allowed cities and geographic validation data.
+///
+public class LocationsDbContext : BaseDbContext
+{
+ public DbSet AllowedCities => Set();
+
+ ///
+ /// Initializes a new instance of the class for design-time operations (migrations).
+ ///
+ /// The options to be used by the DbContext.
+ public LocationsDbContext(DbContextOptions options) : base(options)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class for runtime with dependency injection.
+ ///
+ /// The options to be used by the DbContext.
+ /// The domain event processor.
+ public LocationsDbContext(DbContextOptions options, IDomainEventProcessor domainEventProcessor) : base(options, domainEventProcessor)
+ {
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ // Set default schema for this module
+ modelBuilder.HasDefaultSchema("locations");
+
+ // Apply all configurations from current assembly
+ modelBuilder.ApplyConfigurationsFromAssembly(typeof(LocationsDbContext).Assembly);
+
+ base.OnModelCreating(modelBuilder);
+ }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ base.OnConfiguring(optionsBuilder);
+
+ // Suppress pending model changes warning in test environment
+ // This is needed because test environments may have slightly different configurations
+ var isTestEnvironment = Environment.GetEnvironmentVariable("INTEGRATION_TESTS") == "true";
+ if (isTestEnvironment)
+ {
+ optionsBuilder.ConfigureWarnings(warnings =>
+ warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
+ }
+ }
+
+ protected override Task> GetDomainEventsAsync(CancellationToken cancellationToken = default)
+ {
+ // Locations module currently has no entities with domain events
+ // AllowedCity is a simple CRUD entity without business events
+ return Task.FromResult(new List());
+ }
+
+ protected override void ClearDomainEvents()
+ {
+ // No domain events to clear in Locations module
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Persistence/LocationsDbContextFactory.cs b/src/Modules/Locations/Infrastructure/Persistence/LocationsDbContextFactory.cs
new file mode 100644
index 000000000..96a8d158a
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Persistence/LocationsDbContextFactory.cs
@@ -0,0 +1,87 @@
+using MeAjudaAi.Shared.Database;
+using Microsoft.EntityFrameworkCore;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Persistence;
+
+///
+/// Factory para criação do LocationsDbContext em design time (para migrações)
+///
+///
+/// IMPORTANTE: Este pattern é essencial para migrations do EF Core funcionarem corretamente.
+/// O namespace `MeAjudaAi.Modules.Locations.Infrastructure.Persistence` permite que
+/// a BaseDesignTimeDbContextFactory detecte automaticamente:
+/// - Module name: "Locations" (do namespace)
+/// - Schema: "locations" (lowercase)
+/// - Migrations assembly: "MeAjudaAi.Modules.Locations.Infrastructure"
+///
+public class LocationsDbContextFactory : BaseDesignTimeDbContextFactory
+{
+ protected override string GetDesignTimeConnectionString()
+ {
+ // Obter de variáveis de ambiente ou user secrets
+ // Para configurar: dotnet user-secrets set "ConnectionStrings:Locations" "Host=..."
+ // Ou definir variável de ambiente: LOCATIONS_CONNECTION_STRING
+ var connectionString = Environment.GetEnvironmentVariable("LOCATIONS_CONNECTION_STRING")
+ ?? Environment.GetEnvironmentVariable("ConnectionStrings__Locations");
+
+ if (!string.IsNullOrEmpty(connectionString))
+ {
+ return connectionString;
+ }
+
+ // Construir a partir de componentes individuais
+ var host = Environment.GetEnvironmentVariable("POSTGRES_HOST");
+ var port = Environment.GetEnvironmentVariable("POSTGRES_PORT");
+ var database = Environment.GetEnvironmentVariable("POSTGRES_DB");
+ var username = Environment.GetEnvironmentVariable("POSTGRES_USER");
+ var password = Environment.GetEnvironmentVariable("POSTGRES_PASSWORD");
+
+ // Permitir valores padrão APENAS em ambiente de desenvolvimento local
+ var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
+ var isDevelopment = environment.Equals("Development", StringComparison.OrdinalIgnoreCase);
+
+ if (isDevelopment)
+ {
+ // Valores padrão para desenvolvimento local apenas
+ host ??= "localhost";
+ port ??= "5432";
+ database ??= "meajudaai_dev";
+ username ??= "postgres";
+ password ??= "postgres";
+
+ Console.WriteLine("[WARNING] Using default connection values for Development environment.");
+ Console.WriteLine(" Configure environment variables or user secrets for production.");
+ }
+ else
+ {
+ // Em ambientes não-dev, EXIGIR configuração explícita
+ if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(password))
+ {
+ throw new InvalidOperationException(
+ "Missing required database connection configuration for migrations. " +
+ "Set LOCATIONS_CONNECTION_STRING or POSTGRES_HOST/POSTGRES_PASSWORD environment variables.");
+ }
+
+ port ??= "5432";
+ database ??= "meajudaai";
+ username ??= "postgres";
+ }
+
+ return $"Host={host};Port={port};Database={database};Username={username};Password={password}";
+ }
+
+ protected override string GetMigrationsAssembly()
+ {
+ return "MeAjudaAi.Modules.Locations.Infrastructure";
+ }
+
+ protected override string GetMigrationsHistorySchema()
+ {
+ return "locations";
+ }
+
+ protected override LocationsDbContext CreateDbContextInstance(DbContextOptions options)
+ {
+ return new LocationsDbContext(options);
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Repositories/AllowedCityRepository.cs b/src/Modules/Locations/Infrastructure/Repositories/AllowedCityRepository.cs
new file mode 100644
index 000000000..61557164d
--- /dev/null
+++ b/src/Modules/Locations/Infrastructure/Repositories/AllowedCityRepository.cs
@@ -0,0 +1,92 @@
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using MeAjudaAi.Modules.Locations.Infrastructure.Persistence;
+using Microsoft.EntityFrameworkCore;
+
+namespace MeAjudaAi.Modules.Locations.Infrastructure.Repositories;
+
+///
+/// Repository implementation for AllowedCity entity
+///
+public sealed class AllowedCityRepository(LocationsDbContext context) : IAllowedCityRepository
+{
+ public async Task> GetAllActiveAsync(CancellationToken cancellationToken = default)
+ {
+ return await context.AllowedCities
+ .Where(x => x.IsActive)
+ .OrderBy(x => x.StateSigla)
+ .ThenBy(x => x.CityName)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task> GetAllAsync(CancellationToken cancellationToken = default)
+ {
+ return await context.AllowedCities
+ .OrderBy(x => x.StateSigla)
+ .ThenBy(x => x.CityName)
+ .AsNoTracking()
+ .ToListAsync(cancellationToken);
+ }
+
+ public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
+ {
+ return await context.AllowedCities
+ .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
+ }
+
+ public async Task GetByCityAndStateAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default)
+ {
+ var normalizedCity = cityName?.Trim() ?? string.Empty;
+ var normalizedState = stateSigla?.Trim().ToUpperInvariant() ?? string.Empty;
+
+ return await context.AllowedCities
+ .FirstOrDefaultAsync(x =>
+ EF.Functions.ILike(x.CityName, normalizedCity) &&
+ x.StateSigla == normalizedState,
+ cancellationToken);
+ }
+
+ public async Task IsCityAllowedAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default)
+ {
+ var normalizedCity = cityName?.Trim() ?? string.Empty;
+ var normalizedState = stateSigla?.Trim().ToUpperInvariant() ?? string.Empty;
+
+ return await context.AllowedCities
+ .AnyAsync(x =>
+ EF.Functions.ILike(x.CityName, normalizedCity) &&
+ x.StateSigla == normalizedState &&
+ x.IsActive,
+ cancellationToken);
+ }
+
+ public async Task AddAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default)
+ {
+ await context.AllowedCities.AddAsync(allowedCity, cancellationToken);
+ await context.SaveChangesAsync(cancellationToken);
+ }
+
+ public async Task UpdateAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default)
+ {
+ context.AllowedCities.Update(allowedCity);
+ await context.SaveChangesAsync(cancellationToken);
+ }
+
+ public async Task DeleteAsync(AllowedCity allowedCity, CancellationToken cancellationToken = default)
+ {
+ context.AllowedCities.Remove(allowedCity);
+ await context.SaveChangesAsync(cancellationToken);
+ }
+
+ public async Task ExistsAsync(string cityName, string stateSigla, CancellationToken cancellationToken = default)
+ {
+ var normalizedCity = cityName?.Trim() ?? string.Empty;
+ var normalizedState = stateSigla?.Trim().ToUpperInvariant() ?? string.Empty;
+
+ return await context.AllowedCities
+ .AnyAsync(x =>
+ EF.Functions.ILike(x.CityName, normalizedCity) &&
+ x.StateSigla == normalizedState,
+ cancellationToken);
+ }
+}
diff --git a/src/Modules/Locations/Infrastructure/Services/GeographicValidationService.cs b/src/Modules/Locations/Infrastructure/Services/GeographicValidationService.cs
index 93ae055f6..12da035fb 100644
--- a/src/Modules/Locations/Infrastructure/Services/GeographicValidationService.cs
+++ b/src/Modules/Locations/Infrastructure/Services/GeographicValidationService.cs
@@ -7,6 +7,7 @@ namespace MeAjudaAi.Modules.Locations.Infrastructure.Services;
///
/// Adapter que implementa IGeographicValidationService delegando para IIbgeService.
/// Bridge entre Shared (middleware) e módulo Locations (IBGE).
+/// NOTA: O parâmetro allowedCities é ignorado - a validação agora usa o banco de dados (tabela AllowedCities).
///
public sealed class GeographicValidationService(
IIbgeService ibgeService,
@@ -15,20 +16,19 @@ public sealed class GeographicValidationService(
public async Task ValidateCityAsync(
string cityName,
string? stateSigla,
- IReadOnlyCollection allowedCities,
+ IReadOnlyCollection allowedCities, // IGNORADO: usar banco de dados
CancellationToken cancellationToken = default)
{
logger.LogDebug(
- "GeographicValidationService: Validando cidade {CityName} (UF: {State})",
+ "GeographicValidationService: Validando cidade {CityName} (UF: {State}) usando banco de dados",
cityName,
stateSigla ?? "N/A");
- // Delegar para o IbgeService
+ // Delegar para o IbgeService (que agora usa o repositório)
// Exceções são propagadas para o middleware decidir (fail-open com fallback)
var isAllowed = await ibgeService.ValidateCityInAllowedRegionsAsync(
cityName,
stateSigla,
- allowedCities,
cancellationToken);
return isAllowed;
diff --git a/src/Modules/Locations/Infrastructure/Services/IbgeService.cs b/src/Modules/Locations/Infrastructure/Services/IbgeService.cs
index 828b6f533..f9378dd41 100644
--- a/src/Modules/Locations/Infrastructure/Services/IbgeService.cs
+++ b/src/Modules/Locations/Infrastructure/Services/IbgeService.cs
@@ -1,6 +1,7 @@
using MeAjudaAi.Modules.Locations.Application.Services;
using MeAjudaAi.Modules.Locations.Domain.Exceptions;
using MeAjudaAi.Modules.Locations.Domain.ExternalModels.IBGE;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
using MeAjudaAi.Modules.Locations.Infrastructure.ExternalApis.Clients.Interfaces;
using MeAjudaAi.Shared.Caching;
using Microsoft.Extensions.Caching.Hybrid;
@@ -15,15 +16,15 @@ namespace MeAjudaAi.Modules.Locations.Infrastructure.Services;
public sealed class IbgeService(
IIbgeClient ibgeClient,
ICacheService cacheService,
+ IAllowedCityRepository allowedCityRepository,
ILogger logger) : IIbgeService
{
public async Task ValidateCityInAllowedRegionsAsync(
string cityName,
string? stateSigla,
- IReadOnlyCollection allowedCities,
CancellationToken cancellationToken = default)
{
- logger.LogDebug("Validando cidade {CityName} (UF: {State}) contra lista de cidades permitidas", cityName, stateSigla ?? "N/A");
+ logger.LogDebug("Validando cidade {CityName} (UF: {State}) contra lista de cidades permitidas no banco de dados", cityName, stateSigla ?? "N/A");
// Buscar detalhes do município na API IBGE (com cache)
// Exceções são propagadas para GeographicValidationService -> Middleware (fail-open com fallback)
@@ -36,21 +37,18 @@ public async Task ValidateCityInAllowedRegionsAsync(
}
// Validar se o estado bate (se fornecido)
- if (!string.IsNullOrEmpty(stateSigla))
+ var ufSigla = municipio.GetEstadoSigla();
+ if (!string.IsNullOrEmpty(stateSigla) && !string.Equals(ufSigla, stateSigla, StringComparison.OrdinalIgnoreCase))
{
- var ufSigla = municipio.GetEstadoSigla();
- if (!string.Equals(ufSigla, stateSigla, StringComparison.OrdinalIgnoreCase))
- {
- logger.LogWarning(
- "Município {CityName} encontrado, mas estado não corresponde. Esperado: {ExpectedState}, Encontrado: {FoundState}",
- cityName, stateSigla, ufSigla);
- return false;
- }
+ logger.LogWarning(
+ "Município {CityName} encontrado, mas estado não corresponde. Esperado: {ExpectedState}, Encontrado: {FoundState}",
+ cityName, stateSigla, ufSigla);
+ return false;
}
- // Validar se a cidade está na lista de permitidas (case-insensitive)
- var isAllowed = allowedCities.Any(allowedCity =>
- string.Equals(allowedCity, municipio.Nome, StringComparison.OrdinalIgnoreCase));
+ // Validar se a cidade está na lista de permitidas (usando banco de dados)
+ // ufSigla never null because GetEstadoSigla returns non-nullable string
+ var isAllowed = await allowedCityRepository.IsCityAllowedAsync(municipio.Nome, ufSigla ?? string.Empty, cancellationToken);
if (isAllowed)
{
diff --git a/src/Modules/Locations/Tests/GlobalTestConfiguration.cs b/src/Modules/Locations/Tests/GlobalTestConfiguration.cs
new file mode 100644
index 000000000..7857246a5
--- /dev/null
+++ b/src/Modules/Locations/Tests/GlobalTestConfiguration.cs
@@ -0,0 +1,13 @@
+using MeAjudaAi.Shared.Tests;
+using Xunit;
+
+namespace MeAjudaAi.Modules.Locations.Tests;
+
+///
+/// Collection definition específica para testes de integração do módulo Locations
+///
+[CollectionDefinition("LocationsIntegrationTests")]
+public class LocationsIntegrationTestCollection : ICollectionFixture
+{
+ // Esta classe não tem implementação - apenas define a collection específica do módulo Locations
+}
diff --git a/src/Modules/Locations/Tests/Integration/AllowedCityRepositoryIntegrationTests.cs b/src/Modules/Locations/Tests/Integration/AllowedCityRepositoryIntegrationTests.cs
new file mode 100644
index 000000000..6d5d2d369
--- /dev/null
+++ b/src/Modules/Locations/Tests/Integration/AllowedCityRepositoryIntegrationTests.cs
@@ -0,0 +1,369 @@
+using FluentAssertions;
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using MeAjudaAi.Modules.Locations.Infrastructure.Persistence;
+using MeAjudaAi.Modules.Locations.Infrastructure.Repositories;
+using MeAjudaAi.Shared.Tests.Base;
+using MeAjudaAi.Shared.Tests.Infrastructure;
+using Microsoft.EntityFrameworkCore;
+using Xunit;
+
+namespace MeAjudaAi.Modules.Locations.Tests.Integration;
+
+public class AllowedCityRepositoryIntegrationTests : DatabaseTestBase
+{
+ private AllowedCityRepository _repository = null!;
+ private LocationsDbContext _context = null!;
+
+ public AllowedCityRepositoryIntegrationTests() : base(new TestDatabaseOptions
+ {
+ DatabaseName = "locations_test",
+ Username = "test_user",
+ Password = "test_password",
+ Schema = "locations"
+ })
+ {
+ }
+
+ private async Task InitializeInternalAsync()
+ {
+ await base.InitializeAsync();
+
+ var options = new DbContextOptionsBuilder()
+ .UseNpgsql(ConnectionString)
+ .Options;
+
+ _context = new LocationsDbContext(options);
+ await _context.Database.MigrateAsync();
+
+ _repository = new AllowedCityRepository(_context);
+ }
+
+ [Fact]
+ public async Task AddAsync_WithValidCity_ShouldPersistCity()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+
+ // Act
+ await AddCityAndSaveAsync(city);
+
+ // Assert
+ var savedCity = await _context.AllowedCities.FirstOrDefaultAsync(c => c.Id == city.Id);
+ savedCity.Should().NotBeNull();
+ savedCity!.CityName.Should().Be("Muriaé");
+ savedCity.StateSigla.Should().Be("MG");
+ savedCity.IbgeCode.Should().Be(3143906);
+ savedCity.IsActive.Should().BeTrue();
+ savedCity.CreatedBy.Should().Be("admin@test.com");
+ }
+
+ [Fact]
+ public async Task GetAllActiveAsync_ShouldReturnOnlyActiveCities()
+ {
+ // Arrange
+ var activeCity1 = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ var activeCity2 = new AllowedCity("Itaperuna", "RJ", "admin@test.com", 3302270);
+ var inactiveCity = new AllowedCity("São Paulo", "SP", "admin@test.com", 3550308, false);
+
+ await AddCityAndSaveAsync(activeCity1);
+ await AddCityAndSaveAsync(activeCity2);
+ await AddCityAndSaveAsync(inactiveCity);
+
+ // Act
+ var result = await _repository.GetAllActiveAsync();
+
+ // Assert
+ result.Should().HaveCount(2);
+ result.Should().AllSatisfy(c => c.IsActive.Should().BeTrue());
+ result.Should().Contain(c => c.CityName == "Muriaé");
+ result.Should().Contain(c => c.CityName == "Itaperuna");
+ result.Should().NotContain(c => c.CityName == "São Paulo");
+ }
+
+ [Fact]
+ public async Task GetAllAsync_ShouldReturnAllCities()
+ {
+ // Arrange
+ var activeCity = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ var inactiveCity = new AllowedCity("Itaperuna", "RJ", "admin@test.com", 3302270, false);
+
+ await AddCityAndSaveAsync(activeCity);
+ await AddCityAndSaveAsync(inactiveCity);
+
+ // Act
+ var result = await _repository.GetAllAsync();
+
+ // Assert
+ result.Should().HaveCount(2);
+ result.Should().Contain(c => c.IsActive);
+ result.Should().Contain(c => !c.IsActive);
+ }
+
+ [Fact]
+ public async Task GetByIdAsync_WithExistingCity_ShouldReturnCity()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city);
+
+ // Act
+ var result = await _repository.GetByIdAsync(city.Id);
+
+ // Assert
+ result.Should().NotBeNull();
+ result!.Id.Should().Be(city.Id);
+ result.CityName.Should().Be("Muriaé");
+ result.StateSigla.Should().Be("MG");
+ result.IbgeCode.Should().Be(3143906);
+ }
+
+ [Fact]
+ public async Task GetByIdAsync_WithNonExistingCity_ShouldReturnNull()
+ {
+ // Arrange
+ var nonExistingId = Guid.NewGuid();
+
+ // Act
+ var result = await _repository.GetByIdAsync(nonExistingId);
+
+ // Assert
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task GetByCityAndStateAsync_WithExistingCityAndState_ShouldReturnCity()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city);
+
+ // Act
+ var result = await _repository.GetByCityAndStateAsync("Muriaé", "MG");
+
+ // Assert
+ result.Should().NotBeNull();
+ result!.CityName.Should().Be("Muriaé");
+ result.StateSigla.Should().Be("MG");
+ }
+
+ [Fact]
+ public async Task GetByCityAndStateAsync_WithNonExistingCityAndState_ShouldReturnNull()
+ {
+ // Act
+ var result = await _repository.GetByCityAndStateAsync("Não Existe", "XX");
+
+ // Assert
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task IsCityAllowedAsync_WithActiveCity_ShouldReturnTrue()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city);
+
+ // Act
+ var result = await _repository.IsCityAllowedAsync("Muriaé", "MG");
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task IsCityAllowedAsync_WithInactiveCity_ShouldReturnFalse()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906, false);
+ await AddCityAndSaveAsync(city);
+
+ // Act
+ var result = await _repository.IsCityAllowedAsync("Muriaé", "MG");
+
+ // Assert
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task IsCityAllowedAsync_WithNonExistingCity_ShouldReturnFalse()
+ {
+ // Act
+ var result = await _repository.IsCityAllowedAsync("Não Existe", "XX");
+
+ // Assert
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task UpdateAsync_WithValidChanges_ShouldPersistChanges()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city);
+
+ // Act
+ city.Update("Itaperuna", "RJ", 3302270, true, "admin2@test.com");
+ await UpdateCityAndSaveAsync(city);
+
+ // Assert
+ var updatedCity = await _context.AllowedCities.FirstOrDefaultAsync(c => c.Id == city.Id);
+ updatedCity.Should().NotBeNull();
+ updatedCity!.CityName.Should().Be("Itaperuna");
+ updatedCity.StateSigla.Should().Be("RJ");
+ updatedCity.IbgeCode.Should().Be(3302270);
+ updatedCity.UpdatedBy.Should().Be("admin2@test.com");
+ updatedCity.UpdatedAt.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task DeleteAsync_WithExistingCity_ShouldRemoveCity()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city);
+
+ // Act
+ await _repository.DeleteAsync(city);
+ await _context.SaveChangesAsync();
+
+ // Assert
+ var deletedCity = await _context.AllowedCities.FirstOrDefaultAsync(c => c.Id == city.Id);
+ deletedCity.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task ExistsAsync_WithExistingCity_ShouldReturnTrue()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city);
+
+ // Act
+ var result = await _repository.ExistsAsync("Muriaé", "MG");
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
+ [Fact]
+ public async Task ExistsAsync_WithNonExistingCity_ShouldReturnFalse()
+ {
+ // Act
+ var result = await _repository.ExistsAsync("Não Existe", "XX");
+
+ // Assert
+ result.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task GetAllActiveAsync_ShouldReturnOrderedByCityNameAndState()
+ {
+ // Arrange
+ var city1 = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ var city2 = new AllowedCity("Itaperuna", "RJ", "admin@test.com", 3302270);
+ var city3 = new AllowedCity("Bom Jesus do Itabapoana", "RJ", "admin@test.com", 3300704);
+
+ await AddCityAndSaveAsync(city1);
+ await AddCityAndSaveAsync(city2);
+ await AddCityAndSaveAsync(city3);
+
+ // Act
+ var result = await _repository.GetAllActiveAsync();
+
+ // Assert
+ result.Should().HaveCount(3);
+ // Repository orders by StateSigla first, then CityName
+ result[0].StateSigla.Should().Be("MG");
+ result[0].CityName.Should().Be("Muriaé");
+ result[1].StateSigla.Should().Be("RJ");
+ result[1].CityName.Should().Be("Bom Jesus do Itabapoana");
+ result[2].StateSigla.Should().Be("RJ");
+ result[2].CityName.Should().Be("Itaperuna");
+ }
+
+ [Fact]
+ public async Task GetByCityAndStateAsync_ShouldBeCaseInsensitive()
+ {
+ // Arrange
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city);
+
+ // Act
+ var result1 = await _repository.GetByCityAndStateAsync("MURIAÉ", "mg");
+ var result2 = await _repository.GetByCityAndStateAsync("muriaé", "MG");
+
+ // Assert
+ result1.Should().NotBeNull();
+ result1!.CityName.Should().Be("Muriaé");
+ result2.Should().NotBeNull();
+ result2!.CityName.Should().Be("Muriaé");
+ }
+
+ [Fact]
+ public async Task AddAsync_WithDuplicateCityAndState_ShouldThrowException()
+ {
+ // Arrange
+ var city1 = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city1);
+
+ var city2 = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+
+ // Act
+ var act = async () =>
+ {
+ await _repository.AddAsync(city2);
+ await _context.SaveChangesAsync();
+ };
+
+ // Assert
+ await act.Should().ThrowAsync();
+ }
+
+ [Fact]
+ public async Task AddAsync_WithDuplicateIbgeCode_ShouldThrowException()
+ {
+ // Arrange
+ var city1 = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ await AddCityAndSaveAsync(city1);
+
+ var city2 = new AllowedCity("Outra Cidade", "SP", "admin@test.com", 3143906);
+
+ // Act
+ var act = async () =>
+ {
+ await _repository.AddAsync(city2);
+ await _context.SaveChangesAsync();
+ };
+
+ // Assert
+ await act.Should().ThrowAsync();
+ }
+
+ public override async ValueTask InitializeAsync()
+ {
+ await InitializeInternalAsync();
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ await DisposeInternalAsync();
+ }
+
+ private async Task DisposeInternalAsync()
+ {
+ await _context.DisposeAsync();
+ await base.DisposeAsync();
+ }
+
+ private async Task AddCityAndSaveAsync(AllowedCity city)
+ {
+ await _repository.AddAsync(city);
+ await _context.SaveChangesAsync();
+ }
+
+ private async Task UpdateCityAndSaveAsync(AllowedCity city)
+ {
+ await _repository.UpdateAsync(city);
+ await _context.SaveChangesAsync();
+ }
+}
diff --git a/src/Modules/Locations/Tests/MeAjudaAi.Modules.Locations.Tests.csproj b/src/Modules/Locations/Tests/MeAjudaAi.Modules.Locations.Tests.csproj
index 729b1ab2b..ee3a3f581 100644
--- a/src/Modules/Locations/Tests/MeAjudaAi.Modules.Locations.Tests.csproj
+++ b/src/Modules/Locations/Tests/MeAjudaAi.Modules.Locations.Tests.csproj
@@ -15,12 +15,22 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Modules/Locations/Tests/Unit/Application/Handlers/CreateAllowedCityHandlerTests.cs b/src/Modules/Locations/Tests/Unit/Application/Handlers/CreateAllowedCityHandlerTests.cs
new file mode 100644
index 000000000..2812482e8
--- /dev/null
+++ b/src/Modules/Locations/Tests/Unit/Application/Handlers/CreateAllowedCityHandlerTests.cs
@@ -0,0 +1,145 @@
+using FluentAssertions;
+using MeAjudaAi.Modules.Locations.Application.Commands;
+using MeAjudaAi.Modules.Locations.Application.Handlers;
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using MeAjudaAi.Modules.Locations.Domain.Exceptions;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using Microsoft.AspNetCore.Http;
+using Moq;
+using System.Security.Claims;
+using Xunit;
+
+namespace MeAjudaAi.Modules.Locations.Tests.Unit.Application.Handlers;
+
+public class CreateAllowedCityHandlerTests
+{
+ private readonly Mock _repositoryMock;
+ private readonly Mock _httpContextAccessorMock;
+ private readonly CreateAllowedCityHandler _handler;
+
+ public CreateAllowedCityHandlerTests()
+ {
+ _repositoryMock = new Mock();
+ _httpContextAccessorMock = new Mock();
+ _handler = new CreateAllowedCityHandler(_repositoryMock.Object, _httpContextAccessorMock.Object);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithValidCommand_ShouldCreateAllowedCityAndReturnId()
+ {
+ // Arrange
+ var command = new CreateAllowedCityCommand("Muriaé", "MG", 3143906, true);
+ var userEmail = "admin@test.com";
+
+ SetupHttpContext(userEmail);
+ _repositoryMock.Setup(x => x.ExistsAsync(command.CityName, command.StateSigla, It.IsAny()))
+ .ReturnsAsync(false);
+
+ // Act
+ var result = await _handler.HandleAsync(command, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeEmpty();
+ _repositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WhenCityAlreadyExists_ShouldThrowDuplicateAllowedCityException()
+ {
+ // Arrange
+ var command = new CreateAllowedCityCommand("Muriaé", "MG", 3143906, true);
+ SetupHttpContext("admin@test.com");
+
+ _repositoryMock.Setup(x => x.ExistsAsync(command.CityName, command.StateSigla, It.IsAny()))
+ .ReturnsAsync(true);
+
+ // Act
+ var act = async () => await _handler.HandleAsync(command, CancellationToken.None);
+
+ // Assert
+ await act.Should().ThrowAsync()
+ .WithMessage("*já cadastrada*");
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithNoUserEmail_ShouldUseSystemAsCreator()
+ {
+ // Arrange
+ var command = new CreateAllowedCityCommand("Muriaé", "MG", 3143906, true);
+ SetupHttpContext(null);
+
+ _repositoryMock.Setup(x => x.ExistsAsync(command.CityName, command.StateSigla, It.IsAny()))
+ .ReturnsAsync(false);
+
+ AllowedCity? capturedCity = null;
+ _repositoryMock.Setup(x => x.AddAsync(It.IsAny(), It.IsAny()))
+ .Callback((city, _) => capturedCity = city)
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await _handler.HandleAsync(command, CancellationToken.None);
+
+ // Assert
+ capturedCity.Should().NotBeNull();
+ capturedCity!.CreatedBy.Should().Be("system");
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithNullIbgeCode_ShouldCreateCity()
+ {
+ // Arrange
+ var command = new CreateAllowedCityCommand("Muriaé", "MG", null, true);
+ SetupHttpContext("admin@test.com");
+
+ _repositoryMock.Setup(x => x.ExistsAsync(command.CityName, command.StateSigla, It.IsAny()))
+ .ReturnsAsync(false);
+
+ AllowedCity? capturedCity = null;
+ _repositoryMock.Setup(x => x.AddAsync(It.IsAny(), It.IsAny()))
+ .Callback((city, _) => capturedCity = city)
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await _handler.HandleAsync(command, CancellationToken.None);
+
+ // Assert
+ capturedCity.Should().NotBeNull();
+ capturedCity!.IbgeCode.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithIsActiveFalse_ShouldCreateInactiveCity()
+ {
+ // Arrange
+ var command = new CreateAllowedCityCommand("Muriaé", "MG", 3143906, false);
+ SetupHttpContext("admin@test.com");
+
+ _repositoryMock.Setup(x => x.ExistsAsync(command.CityName, command.StateSigla, It.IsAny()))
+ .ReturnsAsync(false);
+
+ AllowedCity? capturedCity = null;
+ _repositoryMock.Setup(x => x.AddAsync(It.IsAny(), It.IsAny()))
+ .Callback((city, _) => capturedCity = city)
+ .Returns(Task.CompletedTask);
+
+ // Act
+ await _handler.HandleAsync(command, CancellationToken.None);
+
+ // Assert
+ capturedCity.Should().NotBeNull();
+ capturedCity!.IsActive.Should().BeFalse();
+ }
+
+ private void SetupHttpContext(string? userEmail)
+ {
+ var claims = userEmail != null
+ ? new List { new(ClaimTypes.Email, userEmail) }
+ : new List();
+
+ var identity = new ClaimsIdentity(claims);
+ var principal = new ClaimsPrincipal(identity);
+ var httpContext = new DefaultHttpContext { User = principal };
+
+ _httpContextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext);
+ }
+}
diff --git a/src/Modules/Locations/Tests/Unit/Application/Handlers/DeleteAllowedCityHandlerTests.cs b/src/Modules/Locations/Tests/Unit/Application/Handlers/DeleteAllowedCityHandlerTests.cs
new file mode 100644
index 000000000..8edd103dd
--- /dev/null
+++ b/src/Modules/Locations/Tests/Unit/Application/Handlers/DeleteAllowedCityHandlerTests.cs
@@ -0,0 +1,75 @@
+using FluentAssertions;
+using MeAjudaAi.Modules.Locations.Application.Commands;
+using MeAjudaAi.Modules.Locations.Application.Handlers;
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using MeAjudaAi.Modules.Locations.Domain.Exceptions;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using Moq;
+using Xunit;
+
+namespace MeAjudaAi.Modules.Locations.Tests.Unit.Application.Handlers;
+
+public class DeleteAllowedCityHandlerTests
+{
+ private readonly Mock _repositoryMock;
+ private readonly DeleteAllowedCityHandler _handler;
+
+ public DeleteAllowedCityHandlerTests()
+ {
+ _repositoryMock = new Mock();
+ _handler = new DeleteAllowedCityHandler(_repositoryMock.Object);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithValidId_ShouldDeleteAllowedCity()
+ {
+ // Arrange
+ var cityId = Guid.NewGuid();
+ var existingCity = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ var command = new DeleteAllowedCityCommand { Id = cityId };
+
+ _repositoryMock.Setup(x => x.GetByIdAsync(cityId, It.IsAny()))
+ .ReturnsAsync(existingCity);
+
+ // Act
+ await _handler.HandleAsync(command, CancellationToken.None);
+
+ // Assert
+ _repositoryMock.Verify(x => x.DeleteAsync(existingCity, It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WhenCityNotFound_ShouldThrowAllowedCityNotFoundException()
+ {
+ // Arrange
+ var command = new DeleteAllowedCityCommand { Id = Guid.NewGuid() };
+
+ _repositoryMock.Setup(x => x.GetByIdAsync(command.Id, It.IsAny()))
+ .ReturnsAsync((AllowedCity?)null);
+
+ // Act
+ var act = async () => await _handler.HandleAsync(command, CancellationToken.None);
+
+ // Assert
+ await act.Should().ThrowAsync()
+ .WithMessage("*não encontrada*");
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithInactiveCity_ShouldStillDelete()
+ {
+ // Arrange
+ var cityId = Guid.NewGuid();
+ var existingCity = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906, false);
+ var command = new DeleteAllowedCityCommand { Id = cityId };
+
+ _repositoryMock.Setup(x => x.GetByIdAsync(cityId, It.IsAny()))
+ .ReturnsAsync(existingCity);
+
+ // Act
+ await _handler.HandleAsync(command, CancellationToken.None);
+
+ // Assert
+ _repositoryMock.Verify(x => x.DeleteAsync(existingCity, It.IsAny()), Times.Once);
+ }
+}
diff --git a/src/Modules/Locations/Tests/Unit/Application/Handlers/GetAllAllowedCitiesHandlerTests.cs b/src/Modules/Locations/Tests/Unit/Application/Handlers/GetAllAllowedCitiesHandlerTests.cs
new file mode 100644
index 000000000..84f39879d
--- /dev/null
+++ b/src/Modules/Locations/Tests/Unit/Application/Handlers/GetAllAllowedCitiesHandlerTests.cs
@@ -0,0 +1,102 @@
+using FluentAssertions;
+using MeAjudaAi.Modules.Locations.Application.DTOs;
+using MeAjudaAi.Modules.Locations.Application.Handlers;
+using MeAjudaAi.Modules.Locations.Application.Queries;
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using Moq;
+using Xunit;
+
+namespace MeAjudaAi.Modules.Locations.Tests.Unit.Application.Handlers;
+
+public class GetAllAllowedCitiesHandlerTests
+{
+ private readonly Mock _repositoryMock;
+ private readonly GetAllAllowedCitiesHandler _handler;
+
+ public GetAllAllowedCitiesHandlerTests()
+ {
+ _repositoryMock = new Mock();
+ _handler = new GetAllAllowedCitiesHandler(_repositoryMock.Object);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithOnlyActiveTrue_ShouldReturnOnlyActiveCities()
+ {
+ // Arrange
+ var query = new GetAllAllowedCitiesQuery { OnlyActive = true };
+ var activeCities = new List
+ {
+ new("Muriaé", "MG", "admin@test.com", 3143906)
+ };
+
+ _repositoryMock.Setup(x => x.GetAllActiveAsync(It.IsAny()))
+ .ReturnsAsync(activeCities);
+
+ // Act
+ var result = await _handler.HandleAsync(query, CancellationToken.None);
+
+ // Assert
+ result.Should().HaveCount(1);
+ result.First().CityName.Should().Be("Muriaé");
+ _repositoryMock.Verify(x => x.GetAllActiveAsync(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithOnlyActiveFalse_ShouldReturnAllCities()
+ {
+ // Arrange
+ var query = new GetAllAllowedCitiesQuery { OnlyActive = false };
+ var allCities = new List
+ {
+ new("Muriaé", "MG", "admin@test.com", 3143906),
+ new("Itaperuna", "RJ", "admin@test.com", 3302270, false)
+ };
+
+ _repositoryMock.Setup(x => x.GetAllAsync(It.IsAny()))
+ .ReturnsAsync(allCities);
+
+ // Act
+ var result = await _handler.HandleAsync(query, CancellationToken.None);
+
+ // Assert
+ result.Should().HaveCount(2);
+ _repositoryMock.Verify(x => x.GetAllAsync(It.IsAny()), Times.Once);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithNoCities_ShouldReturnEmptyList()
+ {
+ // Arrange
+ var query = new GetAllAllowedCitiesQuery { OnlyActive = true };
+ _repositoryMock.Setup(x => x.GetAllActiveAsync(It.IsAny()))
+ .ReturnsAsync(new List());
+
+ // Act
+ var result = await _handler.HandleAsync(query, CancellationToken.None);
+
+ // Assert
+ result.Should().BeEmpty();
+ }
+
+ [Fact]
+ public async Task HandleAsync_ShouldMapPropertiesToDto()
+ {
+ // Arrange
+ var query = new GetAllAllowedCitiesQuery { OnlyActive = true };
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+ _repositoryMock.Setup(x => x.GetAllActiveAsync(It.IsAny()))
+ .ReturnsAsync(new List { city });
+
+ // Act
+ var result = await _handler.HandleAsync(query, CancellationToken.None);
+
+ // Assert
+ var dto = result.First();
+ dto.CityName.Should().Be(city.CityName);
+ dto.StateSigla.Should().Be(city.StateSigla);
+ dto.IbgeCode.Should().Be(city.IbgeCode);
+ dto.IsActive.Should().Be(city.IsActive);
+ dto.CreatedBy.Should().Be(city.CreatedBy);
+ }
+}
diff --git a/src/Modules/Locations/Tests/Unit/Application/Handlers/GetAllowedCityByIdHandlerTests.cs b/src/Modules/Locations/Tests/Unit/Application/Handlers/GetAllowedCityByIdHandlerTests.cs
new file mode 100644
index 000000000..b0569f536
--- /dev/null
+++ b/src/Modules/Locations/Tests/Unit/Application/Handlers/GetAllowedCityByIdHandlerTests.cs
@@ -0,0 +1,102 @@
+using FluentAssertions;
+using MeAjudaAi.Modules.Locations.Application.DTOs;
+using MeAjudaAi.Modules.Locations.Application.Handlers;
+using MeAjudaAi.Modules.Locations.Application.Queries;
+using MeAjudaAi.Modules.Locations.Domain.Entities;
+using MeAjudaAi.Modules.Locations.Domain.Repositories;
+using Moq;
+using Xunit;
+
+namespace MeAjudaAi.Modules.Locations.Tests.Unit.Application.Handlers;
+
+public class GetAllowedCityByIdHandlerTests
+{
+ private readonly Mock _repositoryMock;
+ private readonly GetAllowedCityByIdHandler _handler;
+
+ public GetAllowedCityByIdHandlerTests()
+ {
+ _repositoryMock = new Mock();
+ _handler = new GetAllowedCityByIdHandler(_repositoryMock.Object);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithValidId_ShouldReturnAllowedCityDto()
+ {
+ // Arrange
+ var cityId = Guid.NewGuid();
+ var query = new GetAllowedCityByIdQuery { Id = cityId };
+ var city = new AllowedCity("Muriaé", "MG", "admin@test.com", 3143906);
+
+ _repositoryMock.Setup(x => x.GetByIdAsync(cityId, It.IsAny()))
+ .ReturnsAsync(city);
+
+ // Act
+ var result = await _handler.HandleAsync(query, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result!.CityName.Should().Be("Muriaé");
+ result.StateSigla.Should().Be("MG");
+ result.IbgeCode.Should().Be(3143906);
+ }
+
+ [Fact]
+ public async Task HandleAsync_WithInvalidId_ShouldReturnNull()
+ {
+ // Arrange
+ var query = new GetAllowedCityByIdQuery { Id = Guid.NewGuid() };
+
+ _repositoryMock.Setup(x => x.GetByIdAsync(query.Id, It.IsAny