diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml index 2e9b799b5..e0a742481 100644 --- a/.github/workflows/ci-backend.yml +++ b/.github/workflows/ci-backend.yml @@ -7,6 +7,7 @@ on: - 'src/Modules/**' - 'src/Bootstrapper/**' - 'src/Shared/**' + - 'src/Contracts/**' - 'tests/**' - '!tests/MeAjudaAi.E2E.Tests/**' - '.github/workflows/ci-backend.yml' @@ -20,6 +21,7 @@ on: - 'src/Modules/**' - 'src/Bootstrapper/**' - 'src/Shared/**' + - 'src/Contracts/**' - 'tests/**' - '!tests/MeAjudaAi.E2E.Tests/**' - '.github/workflows/ci-backend.yml' @@ -172,6 +174,7 @@ jobs: "src/Modules/ServiceCatalogs/Tests/MeAjudaAi.Modules.ServiceCatalogs.Tests.csproj" "src/Modules/Locations/Tests/MeAjudaAi.Modules.Locations.Tests.csproj" "src/Modules/Communications/Tests/MeAjudaAi.Modules.Communications.Tests.csproj" + "src/Modules/Ratings/Tests/MeAjudaAi.Modules.Ratings.Tests.csproj" "src/Modules/SearchProviders/Tests/MeAjudaAi.Modules.SearchProviders.Tests.csproj" "tests/MeAjudaAi.Shared.Tests/MeAjudaAi.Shared.Tests.csproj" "tests/MeAjudaAi.ApiService.Tests/MeAjudaAi.ApiService.Tests.csproj" @@ -283,7 +286,7 @@ jobs: thresholds: "90 80" # line branch fail_below_min: true - - name: Add Coverage PR Comment + - name: Generate Coverage PR Comment uses: marocchino/sticky-pull-request-comment@v3 if: always() && github.event_name == 'pull_request' with: @@ -291,6 +294,79 @@ jobs: header: coverage-report path: code-coverage-results.md + openapi-breaking-change-check: + name: OpenAPI Breaking Change Check + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Checkout PR Branch + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.0.x" + + - name: Build API + env: + ASPNETCORE_ENVIRONMENT: Testing + run: | + dotnet build src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj \ + -c Release \ + -p:GenerateDocumentationFile=true + + - name: Install Swashbuckle CLI + run: dotnet tool install -g Swashbuckle.AspNetCore.Cli --version 10.1.5 + + - name: Generate Current OpenAPI Spec + env: + ASPNETCORE_ENVIRONMENT: Testing + run: | + mkdir -p api-current + swagger tofile \ + --output api-current/api-spec.json \ + src/Bootstrapper/MeAjudaAi.ApiService/bin/Release/net10.0/MeAjudaAi.ApiService.dll \ + v1 + + - name: Checkout Base Branch Spec + env: + BASE_REF: ${{ github.base_ref }} + run: | + echo "⚠️ Generating base spec from source using worktree for origin/$BASE_REF..." + + # Cria um worktree para a branch base em diretório irmão + git worktree add ../base-branch origin/$BASE_REF + + # Gera o spec a partir do build da base + cd ../base-branch + export ASPNETCORE_ENVIRONMENT=Testing + dotnet build src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj \ + -c Release \ + -p:GenerateDocumentationFile=true + + swagger tofile \ + --output $GITHUB_WORKSPACE/api-base.json \ + src/Bootstrapper/MeAjudaAi.ApiService/bin/Release/net10.0/MeAjudaAi.ApiService.dll \ + v1 + + cd $GITHUB_WORKSPACE + git worktree remove ../base-branch --force + + # Validação final: falha se não houver spec base + if [ ! -f api-base.json ]; then + echo "❌ CRITICAL: api-base.json could not be generated for $BASE_REF." + exit 1 + fi + + - name: Check for Breaking Changes + uses: oasdiff/oasdiff-action/breaking@v0.0.39 + with: + base: 'api-base.json' + revision: 'api-current/api-spec.json' + fail-on: 'ERR' + security-scan: name: Security Scan runs-on: ubuntu-latest diff --git a/MeAjudaAi.slnx b/MeAjudaAi.slnx index 02881578d..144ae8007 100644 --- a/MeAjudaAi.slnx +++ b/MeAjudaAi.slnx @@ -84,6 +84,22 @@ + + + + + + + + + + + + + + + + diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md deleted file mode 100644 index eba93eb79..000000000 --- a/docs/roadmap-current.md +++ /dev/null @@ -1,1490 +0,0 @@ -## 🎨 Fase 2: Frontend & Experiência - -**Status**: 🔄 Em andamento (Jan–Mar 2026) - -### Objetivo -Desenvolver aplicações frontend usando **React + Next.js** (Customer Web App, Admin Portal) + **React Native** (Mobile App). - -> **📅 Status Atual**: Sprint 9 (Estabilização) encerrada (11 Abr 2026) -> **📝 Decisão Técnica**: Cobertura Global de 70% atingida e reforçada no CI/CD. -> **🎉 MIGRAÇÃO CONCLUÍDA**: Admin Portal migrado de Blazor para React + Next.js na Sprint 8D - ---- - -### 📱 Stack Tecnológico ATUALIZADO (21 Mar 2026) - -> **📝 Decisão Técnica** (5 Fevereiro 2026): -> Stack de Customer App definida como **React 19 + Next.js 15 + Tailwind CSS v4**. -> **Admin Portal**: Migrado de Blazor WASM para React + Next.js na Sprint 8D. -> **Razão**: SEO crítico para Customer App, performance inicial, ecossistema maduro, contratação facilitada. - -**Decisão Estratégica**: Stack unificado em **React + Next.js** para todos os apps web - -**Justificativa**: -- ✅ **SEO**: Customer App precisa aparecer no Google ("eletricista RJ") - Next.js SSR/SSG resolve -- ✅ **Performance**: Initial load rápido crítico para conversão mobile - code splitting + lazy loading -- ✅ **Ecossistema**: Massivo - geolocalização, mapas, pagamentos, qualquer problema já resolvido -- ✅ **Contratação**: Fácil escalar time - React devs abundantes -- ✅ **Mobile**: React Native maduro e testado vs MAUI Hybrid ainda novo -- ✅ **Modern Stack**: React 19 + Tailwind v4 é estado da arte (2026) -- ⚠️ **Trade-off**: DTOs duplicados (C# backend, TS frontend) - mitigado com OpenAPI TypeScript Generator - -**Stack Completa**: - -**Admin Portal** (React - migrado na Sprint 8D): -- React 19 + TypeScript 5.7+ -- Tailwind CSS v4 -- Zustand (state management) -- React Hook Form + Zod -- NextAuth.js (Keycloak OIDC) - -**Customer Web App** (novo): -- React 19 (Server Components + Client Components) -- Next.js 15 (App Router, SSR/SSG) -- TypeScript 5.7+ (strict mode) -- Tailwind CSS v4 (@theme, CSS variables) -- Base UI React (@base-ui/react) - headless components -- Zustand (client state) + TanStack Query v5 (server state) -- React Hook Form + Zod (forms & validation) -- Lucide React (icons) - -**Mobile Customer App** (novo): -- React Native + Expo -- Compartilha componentes com Customer Web App -- Geolocalização nativa -- Notificações push -- Secure Storage para tokens - - -**Shared**: -- **OpenAPI TypeScript Generator**: Sincroniza tipos C# → TypeScript automaticamente - - **Tooling**: `openapi-typescript-codegen` ou `@hey-api/openapi-ts` - - **Trigger**: CI/CD job on `api/swagger/v1/swagger.json` changes - - **Output**: `MeAjudaAi.Web.Customer/types/api/generated/` - - **Versioning**: API versions `v1`, `v2` (breaking changes require version bump) - - **Breaking Change Gating**: OpenAPI diff in CI fails PR without version bump -- Keycloak OIDC (autenticação unificada) -- PostgreSQL (backend único) - -**Estratégia de Compartilhamento de Código (C# → TypeScript)**: - -| Artefato | Fonte Backend | Saída Frontend | Método de Sincronização | -|----------|----------------|-----------------|-------------| -| **DTOs** | `Contracts/*.cs` | `types/api/*.ts` | OpenAPI Generator (auto) | -| **Enums** | `MeAjudaAi.Contracts/Enums/` | `types/enums.ts` | OpenAPI Generator (auto) | -| **Validação** | FluentValidation | Zod schemas | Geração Automática (Sprint 8A) | -| **Constantes** | `MeAjudaAi.Contracts/Constants/` | `lib/constants.ts` | Geração Automática (Sprint 8A) | - -**Plano de Geração**: -1. Implementar ferramenta CLI para converter `MeAjudaAi.Contracts` Enums e Constants em `types/enums.ts` e `lib/constants.ts`. -2. Implementar conversor de metadados FluentValidation para Zod schemas em `types/api/validation.ts`. -3. Adicionar tickets no backlog para verificação em CI e versionamento semântico dos artefatos gerados. - -**Nota de Estratégia**: Priorizamos o reuso de `MeAjudaAi.Contracts` para enums e constantes para manter o Frontend alinhado com o Backend e evitar desvios. - -**Localização dos Arquivos Gerados**: -```text -src/ -├── Contracts/ # Backend DTOs (C#) -└── Web/ - ├── MeAjudaAi.Web.Admin/ # React + Next.js (migrado do Blazor na Sprint 8D) - ├── MeAjudaAi.Web.Customer/ # Next.js - │ └── types/api/generated/ # ← OpenAPI gerado types - └── Mobile/ - └── MeAjudaAi.Mobile.Customer/ # React Native - └── src/types/api/ # ← Mesmo OpenAPI gerado types -``` - -**Pipeline CI/CD** (GitHub Actions): -1. Mudanças no Backend → Swagger JSON atualizado -2. Verificação de OpenAPI diff (breaking changes?) -3. Se houver quebra → Requerer bump de versão da API (`v1` → `v2`) -4. Gerar tipos TypeScript -5. Commit para `types/api/generated/` (auto-commit bot) -6. Testes do Frontend rodam com novos tipos - -### 🗂️ Estrutura de Projetos Atualizada -```text -src/ -├── Web/ -│ ├── MeAjudaAi.Web.Admin/ # React + Next.js Admin Portal (Sprint 8D) -│ └── MeAjudaAi.Web.Customer/ # 🚀 Next.js Customer App (Sprint 8A) -├── Mobile/ -│ └── MeAjudaAi.Mobile.Customer/ # 🚀 React Native + Expo (Sprint 8B) -└── Shared/ - ├── MeAjudaAi.Shared.DTOs/ # DTOs C# (backend) - └── MeAjudaAi.Contracts/ # OpenAPI spec → TypeScript types -``` - -### 🔐 Autenticação Unificada - -**Consistência de Autenticação Cross-Platform**: - -| Aspecto | Admin (React) | Customer Web (Next.js) | Customer Mobile (RN) | -|--------|--------------|------------------------|----------------------| -| **Armazenamento de Token** | HTTP-only cookies | HTTP-only cookies | Secure Storage | -| **Vida útil do Token** | 1h acesso + 24h refresh | 1h acesso + 7d refresh | 1h acesso + 30d refresh | -| **Estratégia de Refresh** | Automática (NextAuth) | Middleware refresh | Background refresh | -| **Role Claims** | `role` claim | `role` claim | `role` claim | -| **Sair (Logout)** | `/api/auth/signout` | `/api/auth/signout` | Revoke + clear storage | - -**Configuração Keycloak**: -- **Realm**: `MeAjudaAi` -- **Clientes**: `meajudaai-admin` (público), `meajudaai-customer` (público) -- **Roles**: `admin`, `customer`, `provider` -- **Formato Token**: JWT (RS256) -- **Vida útil Token**: Acesso 1h, Refresh 30d (configurável por cliente: Admin=24h, Customer=7d, Mobile=30d) - -**Detalhes de Implementação**: -- **Protocolo**: OpenID Connect (OIDC) -- **Provedor de Identidade**: Keycloak -- **Admin Portal**: NextAuth.js v5 (React + Next.js) -- **Customer Web**: NextAuth.js v5 (Next.js) -- **Customer Mobile**: React Native OIDC Client -- **Refresh**: Automático via OIDC interceptor - -**Guia de Migração**: Veja `docs/authentication-migration.md` (a ser criado na Sprint 8A) - - - ---- - -### 🚀 Gestão de Restrições Geográficas - -**Resumo**: Restrições geográficas podem ser configuradas via `appsettings.json` (Fase 1, MVP atual) ou gerenciadas dinamicamente via Blazor Admin Portal com banco de dados (Fase 2, planejado Sprint 7+). O middleware `GeographicRestrictionMiddleware` valida cidades/estados permitidos usando IBGE API. - -**Contexto**: O middleware `GeographicRestrictionMiddleware` suporta configuração dinâmica via `Microsoft.FeatureManagement`. Este recurso foi implementado em duas fases: - -#### ✅ Fase 1: Middleware com appsettings (CONCLUÍDA - Sprint 1 Dia 1, 21 Nov 2025) - -**Implementação Atual**: Restrições geográficas baseadas em `appsettings.json` com middleware HTTP e integração IBGE API. - -**Decisões de Arquitetura**: - -1. **Localização de Código** ✅ **ATUALIZADO 21 Nov 2025** - - ✅ **MOVIDO** `GeographicRestrictionMiddleware` para `ApiService/Middlewares` (específico para API HTTP) - - ✅ **MOVIDO** `GeographicRestrictionOptions` para `ApiService/Options` (configuração lida de appsettings da API) - - ✅ **MOVIDO** `FeatureFlags.cs` para `Shared/Constants` (constantes globais como AuthConstants, ValidationConstants) - - ❌ **DELETADO** `Shared/Configuration/` (pasta vazia após movimentações) - - ❌ **DELETADO** `Shared/Middleware/` (pasta vazia, middleware único movido para ApiService) - - **Justificativa**: - - GeographicRestriction é funcionalidade **exclusiva da API HTTP** (não será usada por Workers/Background Jobs) - - Options são lidas de appsettings que só existem em ApiService - - FeatureFlags são constantes (similar a `AuthConstants.Claims.*`, `ValidationConstants.MaxLength.*`) - - Middlewares genéricos já estão em pastas temáticas (Authorization/Middleware, Logging/, Monitoring/) - -2. **Propósito da Feature Toggle** ✅ - - ✅ **Feature flag ativa/desativa TODA a restrição geográfica** (on/off global) - - ✅ **Cidades individuais controladas via banco de dados** (Sprint 3 - tabela `allowed_regions`) - - ✅ **Arquitetura proposta**: - ```ini - FeatureManagement:GeographicRestriction = true → Liga TODA validação - ↓ - allowed_regions.is_active = true → Ativa cidade ESPECÍFICA - ``` - - **MVP (Sprint 1)**: Feature toggle + appsettings (cidades hardcoded) - - **Sprint 3**: Migração para database-backed + Admin Portal UI - -3. **Remoção de Redundância** ✅ **JÁ REMOVIDO** - - ❌ **REMOVIDO**: Propriedade `GeographicRestrictionOptions.Enabled` (redundante com feature flag) - - ❌ **REMOVIDO**: Verificação `|| !_options.Enabled` do middleware - - ✅ **ÚNICA FONTE DE VERDADE**: `FeatureManagement:GeographicRestriction` (feature toggle) - - **Justificativa**: Ter duas formas de habilitar/desabilitar causa confusão e potenciais conflitos. - - **Benefício**: Menos configurações duplicadas, arquitetura mais clara e segura. - -**Organização de Pastas** (21 Nov 2025): -```text -src/ - Shared/ - Constants/ - FeatureFlags.cs ← MOVIDO de Configuration/ (constantes globais) - AuthConstants.cs (existente) - ValidationConstants.cs (existente) - Authorization/Middleware/ (middlewares de autorização) - Logging/ (LoggingContextMiddleware) - Monitoring/ (BusinessMetricsMiddleware) - Messaging/Handlers/ (MessageRetryMiddleware) - - Bootstrapper/MeAjudaAi.ApiService/ - Middlewares/ - GeographicRestrictionMiddleware.cs ← MOVIDO de Shared/Middleware/ - RateLimitingMiddleware.cs (específico HTTP) - SecurityHeadersMiddleware.cs (específico HTTP) - Options/ - GeographicRestrictionOptions.cs ← MOVIDO de Shared/Configuration/ - RateLimitOptions.cs (existente) - CorsOptions.cs (existente) -``` - -**Resultado Sprint 1**: Middleware funcional com validação via IBGE API, feature toggle integrado, e lista de cidades configurável via appsettings (requer redeploy para alterações). - ---- - -### 🚀 Fase 2 Shipped (Jan-Feb 2026) - -#### ✅ Fase 2: Database-Backed + Admin Portal UI (CONCLUÍDO - Sprint 7, 7 Jan 2026) - -**Contexto**: Migrar lista de cidades/estados de `appsettings.json` para banco de dados, permitindo gestão dinâmica via Admin Portal (React) sem necessidade de redeploy. - -**Status**: ✅ IMPLEMENTADO - AllowedCities UI completa com CRUD, coordenadas geográficas, e raio de serviço. - -**Arquitetura Proposta**: -```sql --- Schema: geographic_restrictions (novo) -CREATE TABLE geographic_restrictions.allowed_regions ( - region_id UUID PRIMARY KEY, - type VARCHAR(10) NOT NULL, -- 'City' ou 'State' - city_name VARCHAR(200), - state_code VARCHAR(2) NOT NULL, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - added_at TIMESTAMP NOT NULL, - added_by_user_id UUID, - notes TEXT -); - -CREATE INDEX idx_allowed_regions_state ON geographic_restrictions.allowed_regions(state_code); -CREATE INDEX idx_allowed_regions_active ON geographic_restrictions.allowed_regions(is_active); -``` - -### 🚀 Fase 2 Seguimentos (Mar 2026+) - -**Funcionalidades Admin Portal**: - -- [ ] **Visualização de Restrições Atuais** (Admin Portal React) - - [ ] Tabela com cidades/estados permitidos - - [ ] Filtros: Tipo (Cidade/Estado), Estado, Status (Ativo/Inativo) - - [ ] Ordenação: Alfabética, Data de Adição - - [ ] Indicador visual: Badges para "Cidade" vs "Estado" - -- [ ] **Adicionar Cidade/Estado** - - [ ] Formulário com campos: - - Tipo: Dropdown (Cidade, Estado) - - Estado: Dropdown preenchido via IBGE API (27 UFs) - - Cidade: Autocomplete via IBGE API (se tipo=Cidade) - - Notas: Campo opcional (ex: "Piloto Beta Q1 2025") - - [ ] Validações: - - Estado deve ser sigla válida (RJ, SP, MG, etc.) - - Cidade deve existir no IBGE (validação server-side) - - Não permitir duplicatas (cidade+estado único) - - [ ] Preview: "Você está adicionando: Muriaé/MG" - -- [ ] **Editar Região** - - [ ] Apenas permitir editar "Notas" e "Status" - - [ ] Cidade/Estado são imutáveis (deletar + re-adicionar se necessário) - - [ ] Confirmação antes de desativar região com prestadores ativos - -- [ ] **Ativar/Desativar Região** - - [ ] Toggle switch inline na tabela - - [ ] Confirmação: "Desativar [Cidade/Estado] irá bloquear novos registros. Prestadores existentes não serão afetados." - - [ ] Audit log: Registrar quem ativou/desativou e quando - -- [ ] **Remover Região** - - [ ] Botão de exclusão com confirmação dupla - - [ ] Validação: Bloquear remoção se houver prestadores registrados nesta região - - [ ] Mensagem: "Não é possível remover [Cidade]. Existem 15 prestadores registrados." - -**Integração com Middleware** (Refatoração Necessária): - -**Abordagem 1: Database-First (Recomendado)** -```csharp -// GeographicRestrictionOptions (modificado) -public class GeographicRestrictionOptions -{ - public bool Enabled { get; set; } - public string BlockedMessage { get; set; } = "..."; - - // DEPRECATED: Remover após migração para database - [Obsolete("Use database-backed AllowedRegionsService instead")] - public List AllowedCities { get; set; } = new(); - [Obsolete("Use database-backed AllowedRegionsService instead")] - public List AllowedStates { get; set; } = new(); -} - -// Novo serviço -public interface IAllowedRegionsService -{ - Task> GetAllowedCitiesAsync(CancellationToken ct = default); - Task> GetAllowedStatesAsync(CancellationToken ct = default); -} - -// GeographicRestrictionMiddleware (modificado) -public class GeographicRestrictionMiddleware -{ - private readonly IAllowedRegionsService _regionsService; - - public async Task InvokeAsync(HttpContext context) - { - // Buscar listas do banco (com cache) - var allowedCities = await _regionsService.GetAllowedCitiesAsync(ct); - var allowedStates = await _regionsService.GetAllowedStatesAsync(ct); - - // Lógica de validação permanece igual - if (!allowedCities.Contains(userCity) && !allowedStates.Contains(userState)) - { - // Bloquear - } - } -} -``` - -**Abordagem 2: Híbrida (Fallback para appsettings)** -- Se banco estiver vazio, usar `appsettings.json` -- Migração gradual: Admin adiciona regiões no portal, depois remove de appsettings - -**Estratégia de Cache**: -- Usar `HybridCache` (já implementado no `IbgeService`) -- TTL: 5 minutos (balanço entre performance e dados frescos) -- Invalidação: Ao adicionar/remover/editar região no portal administrativo - -**Caminho de Migração**: -1. **Sprint 3 Semana 1**: Criar schema `geographic_restrictions` + tabela -2. **Sprint 3 Semana 1**: Implementar `AllowedRegionsService` com cache -3. **Sprint 3 Semana 1**: Refatoração do middleware para usar serviço (mantém fallback appsettings) -4. **Sprint 3 Semana 2**: Implementar endpoints CRUD na API administrativa -5. **Sprint 3 Semana 2**: Implementar UI no Admin Portal (React) -6. **Sprint 3 Pós-Deploy**: Popular banco com dados iniciais (Muriaé, Itaperuna, Linhares) -7. **Sprint 4**: Remover valores de appsettings.json (obsoleto) - -**Testes Necessários**: -- [ ] Unit tests: `AllowedRegionsService` (CRUD + invalidação de cache) -- [ ] Integration tests: Middleware com banco populado vs vazio -- [ ] E2E tests: Admin adiciona cidade → Middleware bloqueia outras cidades - -**Documentação**: -- [ ] Guia do Usuário Admin: Como adicionar/remover cidades piloto -- [ ] Débito Técnico: Marcar `AllowedCities` e `AllowedStates` como obsoletos - -**⚠️ Breaking Changes**: -- ~~`GeographicRestrictionOptions.Enabled` será removido~~ ✅ **JÁ REMOVIDO** (Sprint 1 Dia 1) - - **Motivo**: Redundante com feature toggle - fonte de verdade única - - **Migração**: Usar apenas `FeatureManagement:GeographicRestriction` em appsettings -- `GeographicRestrictionOptions.AllowedCities/AllowedStates` será deprecado (Sprint 3) - - **Migração**: Admin Portal populará tabela `allowed_regions` via UI - -**Estimativa**: -- **Backend (API + Serviço)**: 2 dias -- **Frontend (Admin Portal UI)**: 2 dias -- **Migração + Testes**: 1 dia -- **Total**: 5 dias (dentro da Sprint 3 de 2 semanas) - -#### 7. Moderação de Avaliações (Preparação para Fase 3) -- [ ] **Listagem**: Avaliações sinalizadas/reportadas -- [ ] **Ações**: Aprovar, Remover, Banir usuário -- [ ] Stub para módulo de Avaliações (a ser implementado na Fase 3) - -**Tecnologias (Admin Portal React)**: -- **Framework**: React 19 + TypeScript 5.7+ -- **UI**: Tailwind CSS v4 + Base UI -- **Estado**: Zustand -- **HTTP**: TanStack Query + React Hook Form -- **Gráficos**: Recharts - -**Resultado Esperado**: -- ✅ Admin Portal funcional e responsivo (React) -- ✅ Todas operações CRUD implementadas -- ✅ Dashboard com métricas em tempo real -- ✅ Deploy no Azure Container Apps - ---- - -### 📅 Sprint 8A: Customer App & Nx Setup (2 semanas) ⏳ ATUALIZADO - -**Status**: CONCLUÍDA (5-13 Fev 2026) -**Dependências**: Sprint 7.16 concluído ✅ -**Duração**: 2 semanas - -**Contexto**: Sprint dividida em duas partes para acomodar a migração para Nx monorepo. - ---- - -#### 📱 Parte 1: Desenvolvimento do Customer App (Foco) - -**Home & Busca** (Semana 1): -- [ ] **Página de Destino**: Seção Hero + busca rápida -- [ ] **Busca Geolocalizada**: Campo de endereço/CEP + raio + serviços -- [ ] **Mapa Interativo**: Exibir prestadores no mapa (Leaflet.Blazor) -- [ ] **Listagem de Resultados**: Cards com foto, nome, nota, distância, tier badge -- [ ] **Filtros**: Nota mínima, tier, disponibilidade -- [ ] **Ordenação**: Distância, Nota, Tier - -**Perfil de Prestador** (Semana 1-2): -- [ ] **Visualização**: Foto, nome, descrição, serviços, nota, avaliações -- [ ] **Contato**: Botão WhatsApp, telefone, e-mail (MVP: links externos) -- [ ] **Galeria**: Fotos do trabalho (se disponível) -- [ ] **Avaliações**: Listar avaliações de outros clientes (apenas leitura, escrita na Fase 3) -- [ ] **Meu Perfil**: Editar informações básicas - -#### 🛠️ Parte 2: Configuração do Nx Monorepo -**Status**: 🔄 EM PROGRESSO (Março 2026) -*Nota: Este é um contêiner ampliado que representa múltiplas sprints destinadas à reestruturação modular do front-end web. A "Sprint 8B.2" encapsula a fundação inicial concluída como parte intrínseca deste arco arquitetural.* - -### ✅ Sprint 8B.2 - NX Scaffolding & Migração Inicial (5 - 18 Mar 2026) -**Branch**: `feature/sprint-8b2-monorepo-cleanup` -**Status**: 🔄 EM REVISÃO -*Nota: A atualização final para "✅ CONCLUÍDA" deve ocorrer somente após o merge do PR ou confirmação explícita de finalização do trabalho na branch.* - -**Objetivos**: -1. 🔴 **MUST-HAVE**: **Configuração do NX Monorepo** (Esforço: Grande) - - Inicializar workspace. - - **Migrar** `MeAjudaAi.Web.Customer` existente para `apps/customer-web`. - - **Andaime (Scaffolding)** (placeholders vazios): `apps/provider-web` e `apps/admin-portal`. - - Extrair bibliotecas compartilhadas: `libs/ui`, `libs/auth`, `libs/api-client`. -2. 🔴 **MUST-HAVE**: **Unificação de Mensageria** (Esforço: Médio) - - Remover Azure Service Bus, unificar apenas no RabbitMQ. -3. 🔴 **MUST-HAVE**: **Pacote de Excelência Técnica** (Esforço: Médio) - - [ ] [**TD**] **Automação Keycloak**: `setup-keycloak-clients.ps1` para desenvolvimento local. - - [ ] [**TD**] **Limpeza de Analisadores**: Corrigir avisos SonarLint nos apps React e Contratos. - - [ ] [**TD**] **Refatoração de Extensões**: Extrair `BusinessMetricsMiddlewareExtensions`. - - [ ] [**TD**] **Logging Polly**: Migrar logs de resiliência para ILogger (Issue #113). - - [ ] [**TD**] **Padronização**: Alinhamento de sintaxe de Record em `Contracts`. - *(TODO: Marcar os checkboxes acima como [x] após o merge do PR na branch feature/sprint-8b2-monorepo-cleanup)* - ---- - -**Entregáveis**: -- [ ] Nx workspace com `apps/customer-web` (migrado). -- [ ] Placeholders para `apps/provider-web` e `apps/admin-portal`. -- [ ] Bibliotecas extraídas para `libs/ui`, `libs/auth` e `libs/api-client`. -*(TODO: Marcar os entregáveis como [x] após o merge do PR 153)* - ---- - -## 🔥 Tarefas Técnicas Cross-Module ⏳ ATUALIZADO - -**Status**: ✅ CONCLUÍDO (Sprint 5.5 - 19 Dez 2025) - -**Contexto Atual**: -- ✅ Lock files regenerados em todos os módulos (37 arquivos atualizados) -- ✅ PR #81 (Aspire 13.1.0) atualizado com lock files corretos -- ✅ PR #82 (FeatureManagement 4.4.0) atualizado com lock files corretos -- ⏳ Aguardando validação CI/CD antes do merge -- 📋 Desenvolvimento frontend aguardando conclusão desta sprint - -Tarefas técnicas que devem ser aplicadas em todos os módulos para consistência e melhores práticas. - -### Controle de Migração em Produção - -**Problema**: Implementar controle `APPLY_MIGRATIONS` nos módulos restantes - -**Contexto**: O módulo Documents já implementa controle via variável de ambiente `APPLY_MIGRATIONS` para desabilitar migrations automáticas em produção. - -**Implementação** (padrão estabelecido em `Documents/API/Extensions.cs`): - -```csharp -private static void EnsureDatabaseMigrations(WebApplication app) -{ - // Lê a variável de ambiente (ou de IConfiguration) - var applyMigrations = app.Configuration["APPLY_MIGRATIONS"] - ?? Environment.GetEnvironmentVariable("APPLY_MIGRATIONS"); - - if (!string.IsNullOrEmpty(applyMigrations) && - bool.TryParse(applyMigrations, out var shouldApply) && !shouldApply) - { - app.Logger.LogInformation("Automatic migrations disabled via APPLY_MIGRATIONS=false"); - return; - } - - using var scope = app.Services.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - // Aplicar migrations normalmente - context.Database.Migrate(); -} -``` - -**Status por Módulo**: -- ✅ **Documents**: Implementado (Sprint 4 - 16 Dez 2025) -- ⏳ **Users**: Pendente -- ⏳ **Providers**: Pendente -- ⏳ **ServiceCatalogs**: Pendente -- ⏳ **Locations**: Pendente -- ⏳ **SearchProviders**: Pendente - -**Esforço Estimado**: 15 minutos por módulo (copiar padrão do Documents) - -**Documentação**: Padrão documentado em `docs/database.md` seção "Controle de Migrations em Produção" - -**Prioridade**: MÉDIA - Implementar antes do primeiro deployment em produção - ---- - -## 📋 Sprint 5.5: Arquivos de Lock de Pacote e Atualizações de Dependência (19 Dez 2025) - -**Status**: 🔄 EM ANDAMENTO - Aguardando CI/CD -**Duração**: 1 dia -**Objetivo**: Resolver conflitos de arquivos de lock de pacote e atualizar dependências - -### Contexto - -Durante o processo de atualização automática de dependências pelo Dependabot, foram identificados conflitos nos arquivos `packages.lock.json` causados por incompatibilidade de versões do pacote `Microsoft.OpenApi`. - -**Problema Raiz**: -- Arquivos de lock esperavam versão `[2.3.12, )` -- Central Package Management especificava `[2.3.0, )` -- Isso causava erros NU1004 em todos os projetos, impedindo compilação e testes - -### Ações Executadas - -#### ✅ Correções Implementadas - -1. **Branch feature/refactor-and-cleanup** - - ✅ 37 arquivos `packages.lock.json` regenerados - - ✅ Commit: "chore: regenerate package lock files to fix version conflicts" - - ✅ Push para origin concluído - -2. **Branch master** - - ✅ Merge de feature/refactor-and-cleanup → master - - ✅ Push para origin/master concluído - - ✅ Todos os arquivos de lock atualizados na branch principal - -3. **PR #81 - Atualização do Aspire 13.1.0** - - Branch: `dependabot/nuget/aspire-f7089cdef2` - - ✅ Arquivos de lock regenerados (37 arquivos) - - ✅ Commit: "fix: regenerate package lock files after Aspire 13.1.0 update" - - ✅ Force push concluído - - ⏳ Aguardando CI/CD (Verificações de Qualidade de Código, Escaneamento de Segurança) - -4. **PR #82 - Atualização do FeatureManagement 4.4.0** - - Branch: `dependabot/nuget/Microsoft.FeatureManagement.AspNetCore-4.4.0` - - ✅ Arquivos de lock regenerados (36 arquivos) - - ✅ Commit: "fix: regenerate package lock files after FeatureManagement update" - - ✅ Push concluído - - ⏳ Aguardando CI/CD (Verificações de Qualidade de Código, Escaneamento de Segurança) - -### Próximos Passos - -1. ✅ **Merge dos PRs #81 e #82** - Concluído (19 Dez 2025) -2. ✅ **Atualizar branch de funcionalidade** - Merge master → feature/refactor-and-cleanup -3. ✅ **Criar PR #83** - Branch feature/refactor-and-cleanup → master -4. ⏳ **Aguardar revisão e merge do PR #83** -5. 📋 **Iniciar Sprint 6** - Documentação do GitHub Pages (Q1 2026) -6. 📋 **Planejar Sprint 7** - Blazor Admin Portal (Q1 2026) - -#### ✅ Atualizações de Documentação (19 Dez 2025) - -**Roadmap**: -- ✅ Atualizada seção Sprint 5.5 com todas as ações executadas -- ✅ Atualizado status de Fase 2 para "Em Planejamento - Q1 2026" -- ✅ Atualizados Sprints 3-5 com dependências e novas timelines -- ✅ Atualizada última modificação para 19 de Dezembro de 2025 - -**Limpeza de Modelos**: -- ✅ Removido `.github/pull-request-template-coverage.md` (modelo específico de outro PR) -- ✅ Removida pasta `.github/issue-template/` (issues obsoletas: EFCore.NamingConventions, Npgsql já resolvidas) -- ✅ Criado `.github/pull_request_template.md` (modelo genérico para futuros PRs) -- ✅ Commit: "chore: remove obsolete templates and create proper PR template" - -**Pull Request #83**: -- ✅ PR criado: feature/refactor-and-cleanup → master -- ✅ Título: "feat: refactoring and cleanup sprint 5.5" -- ✅ Descrição atualizada refletindo escopo real (documentação + merge dos PRs #81/#82 + limpeza de modelos) -- ⏳ Aguardando revisão e validação do CI/CD - -### Lições Aprendidas - -- **Dependabot**: Regenerar arquivos de lock manualmente após atualizações de versões com conflitos -- **CI/CD**: Validação rigorosa dos locks de pacote previne implantações quebradas -- **Central Package Management**: Manter sincronização entre arquivos de lock e Directory.Packages.props -- **Gestão de Modelos**: Manter apenas modelos genéricos e reutilizáveis em `.github/` -- **Documentação Primeiro**: Documentar ações executadas imediatamente no roadmap para rastreabilidade - ---- - -### ✅ Sprint 8C - Provider Web App (React + NX) (19 Mar - 21 Mar 2026) -- ✅ **Nx Integration**: `MeAjudaAi.Web.Provider` integrado ao workspace Nx -- ✅ **Integração de Onboarding**: - - `/onboarding/basic-info` conectado à API (`apiMeGet`/`apiMePut`) - - `/onboarding/documents` conectado à API (upload via SAS URL para Azure Blob Storage) -- ✅ **Painel com Dados Reais**: Página principal (`/`) substituída por dados reais via `apiMeGet` -- ✅ **Perfil Público do Prestador**: Nova rota `/provider/[slug]` para perfis públicos com slugs amigáveis ao SEO -- ✅ **Gestão do Perfil do Prestador**: - - `/alterar-dados` - Edição completa via `apiMePut` - - `/configuracoes` - Alternância de visibilidade + exclusão de conta com confirmação LGPD -- ✅ **URLs amigáveis (Slug)**: Perfis públicos acessíveis via slugs (ex: `/provider/joao-silva-a1b2c3d4`) - -### ✅ Sprint 8D - Migração do Portal Administrativo (2 - 24 Mar 2026) - -**Status**: ✅ CONCLUÍDA (24 Mar 2026) -**Foco**: Migração em fases do Blazor WASM para React. - -**Entregáveis**: -- ✅ **Admin Portal React**: `src/Web/MeAjudaAi.Web.Admin/` funcional em React. -- ✅ **CRUD de Prestadores**: Gestão completa de prestadores. -- ✅ **Gestão de Documentos**: Envio e verificação de documentos. -- ✅ **Catálogo de Serviços**: Gestão do catálogo de serviços. -- ✅ **Cidades Permitidas**: Gestão de restrições geográficas. -- ✅ **KPIs do Painel**: Painel administrativo com métricas. - -### ✅ Sprint 8E - Testes E2E e Infraestrutura de Teste React (23 Mar - 25 Mar 2026) - -**Status**: ✅ CONCLUÍDA (25 Mar 2026) -**Foco**: Testes E2E (Playwright) + infraestrutura de testes unitários (Vitest + RTL + MSW) + Governança de Cobertura Global. - -**Escopo — E2E (Playwright)** ✅: -1. ✅ **Configuração Playwright**: `playwright.config.ts` com 6 projetos (Chromium, Firefox, WebKit, Mobile, CI) -2. ✅ **Customer E2E** (5 especificações): auth, onboarding, performance, profile, search -3. ✅ **Provider E2E** (5 especificações): auth, dashboard, onboarding, performance, profile-mgmt -4. ✅ **Admin E2E** (5 especificações): auth, configs, dashboard, mobile-responsiveness, providers -5. ✅ **Fixtures Compartilhadas**: `src/Web/libs/e2e-support/base.ts` (loginAsAdmin, loginAsProvider, loginAsCustomer, logout) -6. ✅ **Integração CI**: `master-ci-cd.yml` atualizado para gerar especificação OpenAPI e rodar E2E. - -**Escopo — Testes Unitários (Vitest + RTL)** ✅: -7. ✅ **Infraestrutura**: `libs/test-support/` (test-utils.tsx, customRenderHook), limites individuais removidos em favor de Cobertura Global. -8. ✅ **Cobertura Global**: Script `src/Web/scripts/merge-coverage.mjs` consolida relatórios de todos os projetos com limite de 70%. -9. ✅ **Hardening Admin**: Testes unitários para `Sidebar`, `Button`, `Dashboard`, `Providers` e `Users`. Autenticação centralizada em `auth.ts`. -10. ✅ **Hardening Customer**: `DashboardClient` (conformidade com DTO), `DocumentUpload` (asserções da API) e `SearchFilters` (validação de categoria da API). - -**Cenários de Teste E2E**: -- [x] Autenticação (login, logout, refresh token) -- [x] Fluxo de onboarding (Cliente e Prestador) -- [x] CRUD de prestadores e serviços (Admin) -- [x] Busca e filtros geolocalizados -- [x] Responsividade mobile -- [x] Desempenho e Core Web Vitals (INP, LCP, CLS) - -**Pendências para fechar Sprint**: -- [x] Testes unitários Admin (hooks: providers, categories, dashboard, services, allowed-cities, users; components: sidebar, ui) -- [x] Testes unitários Prestador (hooks; components: dashboard cards, profile) -- [x] Configurar MSW handlers para Admin e Prestador - -### ✅ Sprint 9 - BUFFER & Mitigação de Risco (25 Mar - 11 Abr 2026) - -**Status**: ✅ ENCERRADA (Snapshot: encerrada em 11 Abr 2026) -**Duração**: 12 dias de buffer (finalização do MVP) -- Polimento, Refatoração e Correção. -- Mover tarefas Opcionais da 8B.2 para cá se necessário. -- Limite de taxa (Rate limiting) e segurança/monitoramento avançado. - -**Seguimentos Pendentes**: -- [ ] **Gating de Diff OpenAPI**: Adicionar verificação de mudanças de quebra em CI (falhar PR se API mudar sem bump de versão) -- [x] **Módulo de Comunicações**: ~~Implementar infraestrutura base (outbox pattern, modelos, handlers de evento)~~ ✅ Implementado (exceção BUFFER — infraestrutura entregue nesta sprint) - -## 🎯 MVP Final Launch: 12 - 16 de Maio de 2026 🎯 - -### ⚠️ Avaliação e Mitigação de Risco - -#### Estratégia de Mitigação de Risco -- **Contingência de Ramificação**: Se as tarefas principais (Migração Admin, Configuração NX) atrasarem, priorizaremos os fluxos essenciais do Jogador (Cliente/Prestador) e recairemos sobre as soluções Admin existentes. -- **Aplicativos Móveis**: Retirados do escopo do MVP para a Fase 2 para garantir a estabilidade da plataforma web. -- **Buffer**: A Sprint 9 é estritamente para estabilidade, sem novas funcionalidades (Exceção: infraestrutura do Módulo de Comunicações). -- Documentação final para o MVP - -### Cenários de Risco Documentados - -### Cenário de Risco 1: Complexidade de Integração do Keycloak - -- **Problema Potencial**: Fluxos OIDC em Blazor WASM com tokens de atualização podem exigir configuração complexa -- **Impacto**: +2-3 dias além do planejado na Sprint 6 -- **Mitigação Sprint 9**: - - Usar a Sprint 9 para refinar os fluxos de autenticação - - Implementar o tratamento adequado de atualização de token - - Adicionar mecanismos de fallback - -### Cenário de Risco 2: Problemas de Desempenho do React - -- **Problema Potencial**: Tamanho do pacote do aplicativo > 5MB, carregamento lento não configurado corretamente -- **Impacto**: Experiência do Usuário (UX) ruim, +2-3 dias de otimização -- **Mitigação Sprint 9**: - - Divisão de código com importações dinâmicas - - Tree shaking e otimização de pacotes - - SSR/SSG via Next.js para melhorar o carregamento inicial - - Carregamento lento de componentes React - - Otimizar imagens usando next/image e formatos responsivos - -### Cenário de Risco 3: Problemas Específicos da Plataforma MAUI Hybrid (REMOVIDO DO ESCOPO DO MVP) - -> **⚠️ IMPORTANTE**: Este cenário de risco foi removido do escopo do MVP. Os Aplicativos Móveis foram adiados para a Fase 2 conforme nota acima. - -- **Problema Potencial**: Diferenças de comportamento iOS vs Android (permissões, geolocalização, acesso a arquivos) -- **Impacto**: +4-5 dias de depuração específica da plataforma -- **Mitigação Sprint 9**: - - Criar abstrações para APIs específicas da plataforma - - Implementar fallbacks para funcionalidades não suportadas - - Testes em dispositivos reais (não apenas emuladores) - -### Cenário de Risco 4: Casos de Borda da Integração de API - -- **Problema Potencial**: Casos de erro não cobertos (tempos limite, falhas de rede, atualizações simultâneas) -- **Impacto**: +2-3 dias de endurecimento (hardening) -- **Mitigação Sprint 9**: - - Implementar políticas de nova tentativa com Polly - - Adicionar tratamento de concorrência otimista - - Melhorar as mensagens de erro e o feedback do usuário - -### Tarefas Sprint 9 (Executar conforme necessário) - -#### 1. Conclusão do Trabalho em Andamento -- [ ] Completar funcionalidades parciais das Sprints 6-8 -- [ ] Resolver todos os TODOs/FIXMEs adicionados durante a implementação -- [ ] Fechar issues abertas durante o desenvolvimento frontend - -#### 1.1. ✅ 🧪 SearchProviders Testes E2E (Concluído) -- [x] Teste E2E: Buscar prestadores por serviço + raio (2km, 5km, 10km) -- [x] Teste E2E: Validar ordenação por distância crescente -- [x] Teste E2E: Validar restrição geográfica (AllowedCities) - prestadores fora da cidade não aparecem -- [x] Teste E2E: Desempenho (<1500ms em ambiente de teste) -- [x] Teste E2E: Cenário sem resultados (nenhum prestador no raio) -- [x] Teste E2E: Validar paginação de resultados (10, 20, 50 itens por página) - -**Infraestrutura**: -- Usar `TestcontainersFixture` com PostGIS 16-3.4 -- Semear banco de dados com prestadores em localizações conhecidas (lat/lon) -- Usar `HttpClient` para chamar o ponto de extremidade `/api/search-providers/search` -- Validar a resposta JSON com FluentAssertions - -**Critérios de Aceitação**: -- ✅ 6 testes E2E passando com 100% de cobertura dos cenários -- ✅ Desempenho validado -- ✅ Documentação em `docs/testing/e2e-tests.md` -- ✅ CI/CD executando testes E2E na pipeline - -#### 1.2. 🛠️ Excelência Técnica e Débito (Sprint 9) -**Prioridade**: MÉDIA -**Estimativa**: 3-4 dias - -**Objetivo**: Resolver pendências técnicas e melhorar a resiliência do sistema. - -**Entregáveis**: -- [x] **Generalização do Outbox Pattern**: Mover estrutura base (entidade, repositório, worker base) para `MeAjudaAi.Shared` para reuso em futuros módulos (Payments, Bookings). -- [ ] **Handlers de Evento**: Implementar handlers para comunicação entre SearchProviders e ServiceCatalogs. - -- [ ] **Login Social**: Reintegrar login com Instagram via Keycloak OIDC (Issue #141). -- [ ] **Resiliência**: Aplicar `CancellationToken` nos Effects de `ServiceCatalogs`, `Documents` e `Locations`. -- [ ] **Localização (Backend)**: Migrar strings de erro da API para `.resx` e integrar FluentValidation para suporte a multi-idioma via cabeçalhos. -- [ ] **Localização (Frontend React)**: Implementar infraestrutura com `i18next` (JSON) nos apps Admin e Customer, localizando mensagens de validação do **Zod**. -- [ ] **Testes Arquiteturais**: Implementar testes com `NetArchTest` no Módulo de Comunicações para garantir isolamento (evitar referências circulares e acesso direto a DBs externos). -- [ ] **Identidade Visual da Interface**: Aplicar cores oficiais (Azul, Creme, Laranja) e padronizar o tema em todo o Portal Administrativo (React). -- [ ] **Testes Unitários (Débito)**: Testes para descarte de `LocalizationSubscription` e despejo LRU do `PerformanceHelper`. - -#### 2. Melhorias de UX/UI -- [ ] **Estados de Carregamento**: Skeletons em todas as cargas assíncronas -- [ ] **Tratamento de Erros**: Mensagens amigáveis para todos os erros (não mostrar stack traces) - -#### 3. Endurecimento de Segurança e Desempenho -- [ ] **Limite de Taxa da API**: Middleware Aspire (100 req/min por IP, 1000 req/min para usuários autenticados) -- [ ] **CORS**: Configurar origens permitidas (apenas domínios de produção) -- [ ] **Proteção CSRF**: Tokens anti-falsificação em formulários -- [ ] **Cabeçalhos de Segurança**: HSTS, X-Frame-Options, CSP -- [ ] **Otimização do Pacote**: Carregamento lento (lazy loading), compilação AOT, tree shaking -- [ ] **Estratégia de Cache**: Implementar cache HTTP para ativos estáticos - -#### 4. Registro e Monitoramento -- [ ] **Registro do Frontend**: Integração com Application Insights (React + Next.js) -- [ ] **Rastreamento de Erros**: Sentry ou similar para erros em produção -- [ ] **Análises (Analytics)**: Google Analytics ou Plausible para rastreamento de uso -- [ ] **Monitoramento de Desempenho**: Rastreamento de Web Vitals (LCP, FID, CLS) - -#### 5. Documentação Final do MVP -- [ ] **Documentação da API**: Swagger/OpenAPI atualizado com exemplos -- [ ] **Guia do Usuário**: Guia de uso para o Portal Administrativo e o Aplicativo do Cliente -- [ ] **Guia do Desenvolvedor**: Como rodar localmente, como contribuir -- [ ] **Guia de Implantação**: Implantação nos Aplicativos de Contêiner do Azure (modelos ARM ou Bicep) -- [ ] **Lições Aprendidas**: Documentar decisões de arquitetura e compensações (trade-offs) - -#### 6. Módulo de Comunicações (NOVO - Sprint 9) - -**Prioridade**: MÉDIA - Infraestrutura base para funcionalidades pós-MVP -**Objetivo**: Criar módulo unificado de comunicações (e-mail, SMS, push) -**Contexto**: Outros módulos (Avaliações, Pagamentos, Reservas) dependem de infraestrutura de comunicações. Implementar agora evita refatoração depois. - -**Estratégia de Testes (Mandatória)**: - -| Tipo | Escopo | Ferramentas | -|------|--------|------------| -| **Unitários** | Lógica de modelos, cálculo de tentativas (retries), mapeamento de DTOs | ✅ xUnit + FluentAssertions | -| **Integrados** | Persistência do Outbox (PostgreSQL), Handlers de eventos | ✅ xUnit + Respawn + Docker | -| **E2E** | Fluxo completo: Evento → Outbox → Envio simulado → Registro | ✅ Playwright + MSW | -| **Arquiteturais** | Validar que módulos não acessam DB de outros, dependências de contratos | ✅ NetArchTest | - ---- - -**Decisão de Arquitetura** (diferente dos outros módulos): - -| Aspecto | Decisão | -|---------|---------| -| **Padrão** | Módulo Completo + Padrão Orquestrador | -| **Infraestrutura** | Padrão Outbox para garantia de entrega (Mensageria Confiável) | -| **Integração** | Baseada em eventos (consome IntegrationEvents) | -| **API Externa** | Abstração via interface (provedor configurável) | -| **Idempotência** | Garantida via CorrelationId/EventId nos Registros | - ---- - -**Arquitetura de Projetos**: -```text -src/ -├── Shared/ -│ └── MeAjudaAi.Contracts/Modules/Communications/ -│ ├── ICommunicationsModuleApi.cs -│ ├── DTOs/ -│ │ ├── EmailMessageDto.cs -│ │ ├── EmailTemplateDto.cs -│ │ ├── PushMessageDto.cs -│ │ ├── SmsMessageDto.cs -│ │ └── CommunicationLogDto.cs -│ ├── Channels/ -│ │ ├── IEmailChannel.cs -│ │ ├── IPushChannel.cs -│ │ └── ISmsChannel.cs -│ └── Queries/ -│ └── CommunicationLogQuery.cs -│ -├── Modules/ -│ └── Communications/ -│ ├── API/ -│ │ ├── MeAjudaAi.Modules.Communications.API.csproj -│ │ └── Endpoints/ -│ │ └── CommunicationsModuleEndpoints.cs -│ ├── Application/ -│ │ ├── MeAjudaAi.Modules.Communications.Application.csproj -│ │ ├── ModuleApi/ -│ │ │ └── CommunicationsModuleApi.cs -│ │ └── Services/ -│ │ └── Email/ -│ │ └── StubEmailService.cs # Provedor atual (Stubs) -│ ├── Domain/ -│ │ ├── MeAjudaAi.Modules.Communications.Domain.csproj -│ │ └── Entities/ -│ │ ├── OutboxMessage.cs (suporte a ScheduledAt) -│ │ ├── EmailTemplate.cs (flag IsSystemTemplate) -│ │ └── CommunicationLog.cs (suporte a CorrelationId) -│ └── Infrastructure/ -│ ├── MeAjudaAi.Modules.Communications.Infrastructure.csproj -│ └── Persistence/ -│ ├── Configurations/ -│ └── Migrations/ -│ -└── Shared/ - └── Communications/ - └── Templates/ - ├── WelcomeEmail.cshtml - ├── ProviderVerificationApproved.cshtml - └── ProviderVerificationRejected.cshtml -``` - -**Localização de Modelos**: -- **Fonte de Verdade**: Arquivos `.cshtml` em `Shared/Communications/Templates/` (compilados). -- **Sistema de Sobreposição (Override)**: A entidade `EmailTemplate` no banco permite sobrescrever o Assunto (`Subject`) ou trechos do Corpo (`Body`) via Portal Administrativo sem nova implantação. -- **IsSystemTemplate**: Flag para proteger modelos críticos de deleção (Garantido via validação em `EmailTemplateRepository.DeleteAsync`). - ---- - -**Mapeamento de Integração com Eventos**: - -| Evento existente | Ação de Comunicação | -|----------------|-------------------| -| `UserRegisteredIntegrationEvent` | ✅ Enviar e-mail de boas-vindas | -| `ProviderAwaitingVerificationIntegrationEvent` | ✅ Notificar administrador | -| `ProviderVerificationStatusUpdatedIntegrationEvent` | ✅ Notificar prestador | -| `DocumentVerifiedIntegrationEvent` | [ ] Notificar prestador | -| `DocumentRejectedIntegrationEvent` | [ ] Notificar prestador | - ---- - -**Interface ICommunicationsModuleApi (Atualizada)**: -> **Nota**: `ECommunicationPriority` é proveniente de `MeAjudaAi.Contracts.Shared` (não redeclare o enum — use o tipo compartilhado `ECommunicationPriority` diretamente). - -```csharp -public interface ICommunicationsModuleApi : IModuleApi -{ - // E-mail - Task> SendEmailAsync( - EmailMessageDto email, - ECommunicationPriority priority = ECommunicationPriority.Normal, - CancellationToken ct = default); - - Task>> GetTemplatesAsync(CancellationToken ct = default); - - // SMS - Task> SendSmsAsync( - SmsMessageDto sms, - ECommunicationPriority priority = ECommunicationPriority.Normal, - CancellationToken ct = default); - - // Push - Task> SendPushAsync( - PushMessageDto push, - ECommunicationPriority priority = ECommunicationPriority.Normal, - CancellationToken ct = default); - - // Registros (Verificação de idempotência via Identificador de Correlação) - Task>> GetLogsAsync( - CommunicationLogQuery query, - CancellationToken ct = default); -} -``` - ---- - -**Entidades de Domínio**: - -```csharp -// OutboxMessage: Garante entrega e permite agendamento -public class OutboxMessage -{ - public Guid Id { get; } - public string? CorrelationId { get; } // Idempotência - public ECommunicationChannel Channel { get; } - public string Payload { get; } // JSON serializado - public EOutboxMessageStatus Status { get; } // Pending, Processing, Sent, Failed - public int RetryCount { get; } - public DateTime CreatedAt { get; } - public DateTime? ScheduledAt { get; } // Agendamento futuro - public DateTime? SentAt { get; } - public string? ErrorMessage { get; } -} - -// EmailTemplate: Modelos com sistema de sobreposição -public class EmailTemplate -{ - public Guid Id { get; } - public string TemplateKey { get; } // "welcome", "verification-approved" - public string? OverrideKey { get; } // Contexto opcional - public string Subject { get; } - public string HtmlBody { get; } - public string TextBody { get; } - public string Language { get; } // pt-br, en-us - public bool IsSystemTemplate { get; } // Proteção contra exclusão - public DateTime CreatedAt { get; } -} - -// CommunicationLog: Trilha de auditoria + Idempotência -public class CommunicationLog -{ - public Guid Id { get; } - public string CorrelationId { get; } // Idempotência (identificador único) - public ECommunicationChannel Channel { get; } - public string Recipient { get; } - public string? TemplateKey { get; } - public bool IsSuccess { get; } - public DateTime CreatedAt { get; } - public string? ErrorMessage { get; } -} - ---- - -**Infraestrutura - Padrão Outbox**: - -Para garantir que as comunicações não sejam perdidas em caso de falha: - -1. **Processo com Outbox** (garantido): - ```text - Evento Ocorrido → Salvar na Tabela de Outbox (Mesma Transação) → Trabalhador de Background processa Outbox - ``` - -**Melhorias Implementadas**: -- ✅ **Nova tentativa automática**: Com recuo exponencial via Polly. -- ✅ **Priorização**: Mensagens de alta prioridade (ex: Redefinição de Senha) furam a fila. -- ✅ **Idempotência**: Verificação de `CorrelationId` no Registro antes do processamento. -- ✅ **Recuperação**: Mecanismo para resetar mensagens travadas no estado Processing. - ---- - -**Estimativa de Esforço**: - -| Tarefa | Esforço | Status | -|------|--------|-----------| -| 1. Criar estrutura de projetos | 2h | ✅ | -| 2. Interfaces ICommunicationsModuleApi | 2h | ✅ | -| 3. Implementar OutboxMessage (Agendamento) | 5h | ✅ | -| 4. Implementar EmailTemplate (Sistema de sobreposição) | 3h | ✅ | -| 5. Implementar CommunicationLog (CorrelationId) | 2h | ✅ | -| 6. Implementar ModuleApi + Orquestrador | 6h | ✅ | -| 7. Handlers de Canal de Stub (E-mail/Sms/Push) | 5h | ✅ | -| 8. Integração com Eventos Existentes (3/5) | 4h | ✅ | -| 9. Criar modelos básicos (.cshtml) | 3h | ✅ | -| 10. Configuração de DI + Políticas Polly | 3h | ✅ | -| **Total** | **~35h (~5 dias)** | ✅ | - ---- - -**Critérios de Aceitação**: -- ✅ Módulo registrado no ModuleApiRegistry -- ✅ Envio garantido via Padrão Outbox -- ✅ Suporte ao agendamento de mensagens -- ✅ Sistema de modelos híbrido funcional -- ✅ Registros com CorrelationId para evitar duplicidade -- ✅ Integração com mais de 3 IntegrationEvents -- ✅ Priorização de mensagens funcional - ---- - -**Seguimentos Pendentes**: -- [ ] Definir provedores reais (SendGrid, Twilio, Firebase) → **Veja Débito Técnico em docs/technical-debt.md** -- [ ] Interface administrativa para gestão dinâmica de modelos -- [ ] Suporte a anexos via URLs do Armazenamento de Blobs -- [ ] Painel de métricas de entrega e conversão - ---- - -**Resultado Esperado Sprint 9**: -- ✅ MVP pronto para produção e polido -- ✅ Todos os cenários de risco mitigados ou resolvidos -- ✅ Segurança e desempenho endurecidos -- ✅ Documentação completa para usuários e desenvolvedores -- ✅ Monitoramento e observabilidade configurados -- ✅ Módulo de Comunicações implementado (infraestrutura base resiliente) -- 🎯 **PRONTO PARA LANÇAMENTO EM 12-16 DE MAIO DE 2026** - -> **⚠️ CRITICAL**: Se a Sprint 9 não for suficiente para completar todos os itens, considerar atraso no lançamento do MVP ou reduzir o escopo (mover recursos não críticos para o pós-MVP). A qualidade e a estabilidade do MVP são mais importantes que a data de lançamento. - ---- - -## 🎯 Fase 3: Qualidade e Monetização - -### Objetivo -Introduzir sistema de avaliações para ranking, modelo de assinaturas premium via Stripe, e verificação automatizada de documentos. - -### 3.1. 🌟 Módulo Reviews & Ratings (Planejado) - -**Objetivo**: Permitir que clientes avaliem prestadores, influenciando o ranking de busca. - -#### **Arquitetura Proposta** -- **Padrão**: Arquitetura em camadas simples -- **Agregação**: Cálculo de `AverageRating` via eventos de integração (não em tempo real) - -#### **Entidades de Domínio** -```csharp -// Review: Raiz de Agregado -public class Review -{ - public Guid ReviewId { get; } - public Guid ProviderId { get; } - public Guid CustomerId { get; } - public int Rating { get; } // 1-5 - public string? Comment { get; } - public DateTime CreatedAt { get; } - public bool IsFlagged { get; } // Para moderação -} - -// ProviderRating: Agregado (ou parte do modelo de leitura) -public class ProviderRating -{ - public Guid ProviderId { get; } - public decimal AverageRating { get; } - public int TotalReviews { get; } - public DateTime LastUpdated { get; } -} -``` - -#### **API Pública (IReviewsModuleApi)** -```csharp -public interface IReviewsModuleApi : IModuleApi -{ - Task SubmitReviewAsync(SubmitReviewRequest request, CancellationToken ct = default); - Task>> GetReviewsForProviderAsync( - Guid providerId, - int page, - int pageSize, - CancellationToken ct = default); - Task FlagReviewAsync(Guid reviewId, string reason, CancellationToken ct = default); -} -``` - -#### **Implementação** -1. **Esquema**: Criar `meajudaai_reviews` com `reviews`, `provider_ratings` -2. **Ponto de Extremidade de Envio**: Validar que o cliente pode avaliar (serviço contratado?) -3. **Cálculo de Classificação**: Publicar `ReviewAddedIntegrationEvent` → Módulo de busca atualiza `AverageRating` -4. **Moderação**: Sistema de sinalização para avaliações inapropriadas -5. **Testes**: Testes unitários para cálculo de média + testes de integração para envio - ---- - -### 3.2. 💳 Módulo Payments & Billing (Planejado) - -**Objetivo**: Gerenciar assinaturas de prestadores via Stripe (Free, Standard, Gold, Platinum). - -#### **Arquitetura Proposta** -- **Padrão**: Camada Anti-Corrupção (ACL) sobre a API do Stripe -- **Isolamento**: Lógica de domínio protegida de mudanças no Stripe - -#### **Entidades de Domínio** -```csharp -// Subscription: Raiz de Agregado -public class Subscription -{ - public Guid SubscriptionId { get; } - public Guid ProviderId { get; } - public string StripeSubscriptionId { get; } - public ESubscriptionPlan Plan { get; } // Free, Standard, Gold, Platinum - public ESubscriptionStatus Status { get; } // Active, Canceled, PastDue - public DateTime StartDate { get; } - public DateTime? EndDate { get; } -} - -// BillingAttempt: Entidade -public class BillingAttempt -{ - public Guid AttemptId { get; } - public Guid SubscriptionId { get; } - public decimal Amount { get; } - public bool IsSuccessful { get; } - public DateTime AttemptedAt { get; } -} -``` - -#### **API Pública (IBillingModuleApi)** -```csharp -public interface IBillingModuleApi : IModuleApi -{ - Task> CreateCheckoutSessionAsync( - CreateCheckoutRequest request, - CancellationToken ct = default); - Task> GetSubscriptionForProviderAsync( - Guid providerId, - CancellationToken ct = default); -} -``` - -#### **Implementação** -1. **Configuração do Stripe**: Configurar produtos e planos de preços no painel -2. **Ponto de Extremidade de Webhook**: Receber eventos do Stripe (`checkout.session.completed`, `invoice.payment_succeeded`, `customer.subscription.deleted`) -3. **Handlers de Evento**: Atualizar o status da `Subscription` baseado em eventos -4. **Sessão de Checkout**: Gerar URL de checkout para o frontend -5. **Eventos de Integração**: Publicar `SubscriptionTierChangedIntegrationEvent` → Módulo de busca atualiza o ranking -6. **Testes**: Testes de integração com eventos simulados da biblioteca de testes do Stripe - ---- - -### 3.3. 🤖 Documents - Verificação Automatizada (Planejado - Fase 2) - -**Objetivo**: Automatizar a verificação de documentos via OCR e APIs governamentais. - -**Funcionalidades Planejadas**: -- **OCR Inteligente**: Azure AI Vision para extrair texto de documentos -- **Validação de Dados**: Verificação cruzada com dados fornecidos pelo prestador -- **Verificações de Antecedentes**: Integração com APIs de antecedentes criminais -- **Pontuação Automática**: Sistema de pontuação baseado na qualidade dos documentos - -**Trabalhos em Background**: -1. **DocumentUploadedHandler**: Aciona o processamento de OCR -2. **OcrCompletedHandler**: Valida os campos extraídos -3. **VerificationScheduler**: Agenda verificações periódicas - -**Nota**: A infraestrutura básica já existe (campo OcrData, estados de verificação), falta implementar trabalhadores e integrações. - ---- - -### 3.4. 🏷️ Dynamic Service Tags (Planejado - Fase 3) - -**Objetivo**: Exibir tags de serviços baseadas na popularidade real por região. - -**Funcionalidades**: -- **Ponto de extremidade**: `GET /services/top-region?city=SP` (ou lat/lon) -- **Lógica**: Calcular serviços com maior volume de buscas/contratações na região do usuário. -- **Recuo (Fallback)**: Exibir "Top Globais" se os dados regionais forem insuficientes. -- **Cache**: TTL curto (ex: 1h) para manter a relevância sem comprometer o desempenho. - ---- - -## 🚀 Fase 4: Experiência e Engajamento (Pós-MVP) - -### Objetivo -Melhorar a experiência do usuário com agendamentos, comunicações centralizadas e análises avançadas. - -### 4.1. 📅 Módulo Service Requests & Booking (Planejado) - -**Objetivo**: Permitir que clientes solicitem serviços e agendem horários com prestadores. - -#### **Funcionalidades** -- **Solicitação de Serviço**: Cliente descreve necessidade e localização -- **Correspondência (Matching)**: O sistema sugere prestadores compatíveis -- **Agendamento**: Calendário integrado com disponibilidade do prestador -- **Notificações**: Lembretes automáticos via módulo de Comunicações - ---- - -### 4.2. 📊 Módulo Analytics & Reporting (Planejado) - -**Objetivo**: Capturar, processar e visualizar dados de negócio e operacionais. - -#### **Arquitetura Proposta** -- **Padrão**: CQRS + Event Sourcing (para auditoria) -- **Métricas**: Fachada sobre OpenTelemetry/Aspire -- **Auditoria**: Log de eventos imutável de todas as atividades -- **Relatórios**: Modelos de leitura desnormalizados para consultas rápidas - -#### **API Pública (IAnalyticsModuleApi)** -```csharp -public interface IAnalyticsModuleApi : IModuleApi -{ - Task> GetReportAsync(ReportQuery query, CancellationToken ct = default); - Task>> GetAuditHistoryAsync( - AuditLogQuery query, - CancellationToken ct = default); -} -``` - -#### **Exibições de Banco de Dados** -```sql --- vw_provider_summary: Visão holística de cada prestador -CREATE VIEW meajudaai_analytics.vw_provider_summary AS -SELECT - p.provider_id, - p.name, - p.status, - p.join_date, - s.subscription_tier, - pr.average_rating, - pr.total_reviews -FROM providers.providers p -LEFT JOIN meajudaai_billing.subscriptions s ON p.provider_id = s.provider_id -LEFT JOIN meajudaai_reviews.provider_ratings pr ON p.provider_id = pr.provider_id; - --- vw_financial_transactions: Consolidação de eventos financeiros -CREATE VIEW meajudaai_analytics.vw_financial_transactions AS -SELECT - ba.attempt_id AS transaction_id, - s.provider_id, - ba.amount, - s.plan, - ba.is_successful AS status, - ba.attempted_at AS transaction_date -FROM meajudaai_billing.billing_attempts ba -JOIN meajudaai_billing.subscriptions s ON ba.subscription_id = s.subscription_id; - --- vw_audit_log_enriched: Log de auditoria legível -CREATE VIEW meajudaai_analytics.vw_audit_log_enriched AS -SELECT - al.log_id, - al.timestamp, - al.event_name, - al.actor_id, - COALESCE(u.full_name, p.name) AS actor_name, - al.entity_id, - al.details_json -FROM meajudaai_analytics.audit_log al -LEFT JOIN users.users u ON al.actor_id = u.user_id -LEFT JOIN providers.providers p ON al.actor_id = p.provider_id; -``` - -#### **Implementação** -1. **Esquema**: Criar `meajudaai_analytics` com `audit_log`, tabelas de relatórios -2. **Handlers de Evento**: Consumir todos os eventos de integração relevantes -3. **Integração de Métricas**: Expor métricas personalizadas via OpenTelemetry -4. **API de Relatórios**: Pontos de extremidade otimizados para leitura de relatórios -5. **Painéis**: Integração com Aspire Dashboard e Grafana -6. **Testes**: Testes de integração para handlers de evento + testes de desempenho para relatórios - ---- - -## 🎯 Funcionalidades Adicionais Recomendadas (Fase 4+) - -### 🛡️ Admin Portal - Módulos Avançados -**Funcionalidades Adicionais (Pós-MVP)**: -- **Widget de Painel de Atividades Recentes**: Feed cronológico de atividades (registros, envios, verificações, mudanças de status) com atualizações em tempo real via SignalR -- **Análises de Usuário e Prestador**: Painéis avançados com Grafana -- **Detecção de Fraude**: Sistema de pontuação para detectar perfis suspeitos -- **Operações em Lote**: Ações em lote (ex: aprovar múltiplos documentos) -- **Trilha de Auditoria**: Histórico completo de todas as ações administrativas - -#### 📊 Widget de Atividade Recente (Prioridade: MÉDIA) - -**Contexto**: Atualmente, o Painel exibe apenas gráficos estáticos. Um feed de atividades recentes melhoraria a visibilidade operacional. - -**Funcionalidades Core**: -- **Linha do Tempo de Eventos**: Feed cronológico de atividades do sistema -- **Tipos de Eventos**: - - Novos registros de prestadores - - Envios de documentos - - Mudanças de status de verificação - - Ações administrativas (aprovações/rejeições) - - Adições/remoções de serviços -- **Filtros**: Por tipo de evento, módulo, data -- **Atualizações em Tempo Real**: SignalR para atualização automática -- **Paginação**: Carregar mais atividades sob demanda - -**Implementação Técnica**: -```csharp -// Eventos de Domínio → Eventos de Integração → SignalR Hub -public record ProviderRegisteredEvent(Guid ProviderId, string Name, DateTime Timestamp); -public record DocumentUploadedEvent(Guid DocumentId, string Type, DateTime Timestamp); -public record VerificationStatusChangedEvent(Guid ProviderId, string OldStatus, string NewStatus); - -// SignalR Hub -public class ActivityHub : Hub -{ - public async Task BroadcastActivity(ActivityDto activity) - { - await Clients.All.SendAsync("ReceiveActivity", activity); - } -} -``` - -```typescript -// Componente Frontend (React) -import { useEffect, useState } from 'react'; -import * as signalR from '@microsoft/signalr'; - -export function ActivityTimeline() { - const [activities, setActivities] = useState([]); - - useEffect(() => { - const connection = new signalR.HubConnectionBuilder() - .withUrl("/hubs/activity") - .withAutomaticReconnect() - .build(); - - connection.on("ReceiveActivity", (activity: ActivityDto) => { - setActivities(prev => [activity, ...prev]); - }); - - connection.start(); - return () => { connection.stop(); }; - }, []); - - return ( - - {activities.map(activity => ( - - {activity.description} - {formatRelative(activity.timestamp)} - - ))} - - ); -} -``` - -**Estimativa**: 3-5 dias (1 dia para eventos de backend, 1 dia para SignalR, 2-3 dias para frontend) - -**Dependências**: -- SignalR configurado no backend -- Barramento de eventos consumindo eventos de domínio -- Contrato ActivityDto definido - ---- - -### 👤 Gestão de Perfil do Cliente (Alta Prioridade) -**Por quê**: O plano atual é muito focado em prestadores; os clientes também precisam de gestão de perfil. - -**Funcionalidades Core**: -- Editar informações básicas (nome, foto) -- Ver histórico de prestadores contatados -- Gerenciar avaliações escritas -- Preferências de notificações - -**Implementação**: Melhoria (Enhancement) no módulo Users existente - ---- - -### ⚖️ Sistema de Resolução de Disputas (Média Prioridade) -**Por quê**: Mesmo sem pagamentos no aplicativo, podem ocorrer disputas (avaliações injustas, má conduta). - -**Funcionalidades Core**: -- Botão "Reportar" em perfis de prestadores e avaliações -- Formulário para descrever o problema -- Fila no Portal Administrativo para moderadores - -**Implementação**: Novo módulo pequeno ou extensão do módulo de Avaliações - ---- - -## 📊 Métricas de Sucesso - -### 📈 Métricas de Produto -- **Crescimento de usuários**: 20% ao mês -- **Retenção de prestadores**: 85% -- **Satisfação média**: 4.5+ estrelas -- **Taxa de conversão (Grátis → Pago)**: 15% - -### ⚡ Métricas Técnicas (SLOs) - -#### **Metas de Desempenho em Camadas** - -| Categoria | Tempo Alvo | Exemplo | -|-----------|------------|---------| -| **Consultas Simples** | <200ms | Busca por ID, dados em cache | -| **Consultas Médias** | <500ms | Listagens com filtros básicos | -| **Consultas Complexas** | <1000ms | Busca entre módulos, agregações | -| **Consultas Analíticas** | <3000ms | Relatórios, painéis | - -#### **Linha de Base de Desempenho** -- **Assumindo**: Cache distribuído configurado, índices otimizados -- **Revisão Trimestral**: Ajustes baseados em métricas reais - - **Percentis monitorados**: P50, P95, P99 (latência de consultas) - - **Frequência**: Análise e ajuste a cada 3 meses - - **Processo**: Ciclo de feedback → identificar outliers → otimizar consultas lentas -- **Monitoramento**: OpenTelemetry + Aspire Dashboard + Application Insights - -#### **Outros SLOs** -- **Disponibilidade**: 99.9% de tempo de atividade (uptime) -- **Segurança**: Zero vulnerabilidades críticas -- **Cobertura de Testes**: >80% para código crítico - ---- - -## 🔄 Processo de Gestão do Roadmap - -### 📅 Revisão Trimestral -- Avaliação de progresso em relação aos marcos (milestones) -- Ajuste de prioridades baseado em métricas -- Análise de feedback de usuários e prestadores - -### 💬 Feedback Contínuo -- **Contribuição da comunidade**: Pesquisas, suporte, análises -- **Feedback de prestadores**: Portal dedicado para sugestões -- **Necessidades de negócio**: Alinhamento com as partes interessadas (stakeholders) - -### 🎯 Critérios de Priorização -1. **Impacto no MVP**: A funcionalidade é crítica para o lançamento? -2. **Esforço de Implementação**: Complexidade técnica e tempo estimado -3. **Dependências**: Quais módulos dependem desta funcionalidade? -4. **Valor para o Usuário**: Feedback qualitativo e quantitativo - ---- - -## 📋 Sumário Executivo de Prioridades - -### ✅ **Concluído (Set-Dez 2025)** -1. ✅ Sprint 0: Migração .NET 10 + Aspire 13 (21 Nov 2025 - MERGE para master) -2. ✅ Sprint 1: Restrição Geográfica + Integração de Módulos (2 Dez 2025 - MERGE para master) -3. ✅ Sprint 2: Cobertura de Testes de 90.56% (10 Dez 2025) - Meta de 35% SUPERADA em 55.56pp! -4. ✅ Sprint 5.5: Correção de Arquivos de Lock de Pacote (19 Dez 2025) - - Correção de conflitos Microsoft.OpenApi (2.3.12 → 2.3.0) - - 37 arquivos packages.lock.json regenerados - - PRs #81 e #82 atualizados e aguardando merge -5. ✅ Módulo Users (Concluído) -6. ✅ Módulo Providers (Concluído) -7. ✅ Módulo Documents (Concluído) -8. ✅ Módulo Search & Discovery (Concluído) -9. ✅ Módulo Locations - Busca de CEP e geocodificação (Concluído) -10. ✅ Módulo ServiceCatalogs - Catálogo gerenciado por admin (Concluído) -11. ✅ CI/CD - Fluxos de trabalho do GitHub Actions (.NET 10 + Aspire 13) -12. ✅ Ramo (branch) feature/refactor-and-cleanup - Mesclado para master (19 Dez 2025) - -### 📅 Alta Prioridade (Próximos 3 meses - Q1-Q2 2026) -1. ✅ **Sprint 8B.2: NX Monorepo e Excelência Técnica** (Concluída) -2. ✅ **Sprint 8C: App Web do Prestador (React + NX)** (Concluída - 21 Mar 2026) -3. ✅ **Sprint 8D: Migração do Portal Administrativo** (Concluída - 24 Mar 2026) -4. ✅ **Sprint 8E: Testes E2E Apps React (Playwright)** (Concluída - 25 Mar 2026) -5. ⏳ **Sprint 9: BUFFER E MITIGAÇÃO DE RISCO** (25 Mar - 11 Abr 2026) -6. 🎯 **MVP Final Launch: 12 - 16 de Maio de 2026** -7. 📋 Coleções de API - Arquivos .bru do Bruno para todos os módulos - -### 🎯 **Alta Prioridade - Pré-MVP** -1. 🎯 Communications - Notificações por e-mail -2. 💳 Módulo Payments & Billing (Stripe) - Preparação para monetização - -### 🎯 **Média Prioridade (6-12 meses - Fase 2)** -1. 🎉 Módulo Reviews & Ratings -2. 🌍 Documents - Verificação automatizada (OCR + Verificações de antecedentes) -3. 🔄 Search - Trabalhador de indexação para eventos de integração (extensão do módulo SearchProviders) -4. 📊 Analytics - Métricas básicas -5. 🏛️ Sistema de Resolução de Disputas -6. 🔥 Alinhamento de middleware entre UseSharedServices() e UseSharedServicesAsync() - -### 🔬 **Testes E2E Frontend (Pós-MVP)** -**Projeto**: `src/Web` (dividido por projeto) -**Estrutura**: Uma pasta para cada projeto frontend -- `src/Web/MeAjudaAi.Web.Customer/e2e/` - Testes E2E para App Web do Cliente -- `src/Web/MeAjudaAi.Web.Provider/e2e/` - Testes E2E para App Web do Prestador -- `src/Web/MeAjudaAi.Web.Admin/e2e/` - Testes E2E para Portal Administrativo - -**Framework**: Playwright -**Cenários a cobrir**: -- [ ] Autenticação (login, logout, refresh token) -- [ ] Fluxo de onboarding (Cliente e Prestador) -- [ ] CRUD de prestadores e serviços -- [ ] Busca e filtros -- [ ] Responsividade mobile -- [ ] Desempenho e Core Web Vitals diff --git a/docs/roadmap-future.md b/docs/roadmap-future.md deleted file mode 100644 index 74b7e4424..000000000 --- a/docs/roadmap-future.md +++ /dev/null @@ -1,66 +0,0 @@ -### 🔍 **Baixa Prioridade (12+ meses - Fase 3)** -1. 📅 Service Requests & Booking -2. 📱 Mobile Apps (iOS/Android nativo) -3. 🧠 Recomendações com ML -4. 🎮 Gamificação avançada -5. 💬 Chat interno -6. 🌐 Internacionalização (i18n) -7. 🧪 Evolução da Infraestrutura de Testes - ---- - -## 📚 Referências e Recursos - -### 📖 Documentação Relacionada -- **Arquitetura**: [`docs/architecture.md`](./architecture.md) - Princípios e padrões arquiteturais -- **Desenvolvimento**: [`docs/development.md`](./development.md) - Guia de setup e workflow -- **Autenticação**: [`docs/authentication-and-authorization.md`](./authentication-and-authorization.md) - Keycloak e OIDC -- **CI/CD**: [`docs/ci-cd.md`](./ci-cd.md) - Pipeline e deployment - -### 🔥 Ferramentas e Tecnologias -- **.NET 10.0** - Runtime principal (migrado de .NET 9.0) -- **PostgreSQL + PostGIS** - Database com suporte geoespacial -- **Keycloak** - Identity & Access Management -- **Stripe** - Payment processing -- **Azure Blob Storage** - Document storage -- **OpenTelemetry + Aspire** - Observability - -### 🌐 APIs Externas -- **IBGE Localidades API** - Validação oficial de municípios brasileiros - - Base URL: `https://servicodados.ibge.gov.br/api/v1/localidades/` - - Documentação: - - Uso: Validação geográfica para restrição de cidades piloto -- **Nominatim (OpenStreetMap)** - Geocoding (planejado para Sprint 4 - optional improvement) - - Base URL: `https://nominatim.openstreetmap.org/` - - Documentação: - - Uso: Geocoding (lat/lon lookup) para cidades/endereços - - **Note**: Post-MVP feature, não é blocker para geographic-restriction inicial -- **ViaCep API** - Lookup de CEP brasileiro - - Base URL: `https://viacep.com.br/ws/` - - Documentação: -- **BrasilApi CEP** - Lookup de CEP (fallback) - - Base URL: `https://brasilapi.com.br/api/cep/v1/` - - Documentação: -- **OpenCep API** - Lookup de CEP (fallback) - - Base URL: `https://opencep.com/v1/` - - Documentação: - ---- - -## 🧪 Evolução da Infraestrutura de Testes - -O planejamento de qualidade prevê a evolução contínua da pirâmide de testes: - -### Fase 3.1: Cobertura e Contratos (Curto/Médio Prazo) -- **Meta de Cobertura**: Manter >70% de cobertura global no MVP e atingir >80% na Fase 3. -- **Testes de Contrato (Consumer-Driven)**: Implementação de Pact.io para garantir que o frontend Next.js é compatível com a API .NET sem depender de ambiente real. - -### Fase 3.2: E2E Full-Stack & BDD (Médio Prazo) -- **Orquestração com Aspire**: Migração dos testes E2E para rodarem contra instâncias locais orquestradas pelo .NET Aspire (subindo PostgreSQL + PostGIS, API e Redis em containers temporários durante a suíte). -- **BDD (Behavior Driven Development)**: Adoção de Gherkin (Cucumber ou Playwright-BDD) para os fluxos críticos de negócio (ex: ciclo de vida do serviço). - ---- - -*📅 Última atualização: 25 de Março de 2026 (Pós-Sprint 8E)* -*🔄 Roadmap em constante evolução baseado em feedback, métricas e aprendizados* -*📊 Status atual: Sprint 9 🔄 EM ANDAMENTO | MVP Launch em 12-16 de Maio de 2026* diff --git a/docs/roadmap-history.md b/docs/roadmap-history.md index 42a0e1892..04ee93354 100644 --- a/docs/roadmap-history.md +++ b/docs/roadmap-history.md @@ -1,41 +1,64 @@ -# ≡ƒù║∩╕Å Roadmap - MeAjudaAi +# 📜 Histórico do Roadmap - MeAjudaAi -Este documento consolida o planejamento estrat├⌐gico e t├ítico da plataforma MeAjudaAi, definindo fases de implementa├º├úo, m├│dulos priorit├írios e funcionalidades futuras. +Este documento contém o registro de todas as sprints concluídas para fins de auditoria e contexto histórico. --- -## ≡ƒôè Sum├írio Executivo +## ✅ Sprint 9 - BUFFER & Mitigação de Risco (Concluída em 11 Abr 2026) -**Projeto**: MeAjudaAi - Plataforma de Conex├úo entre Clientes e Prestadores de Servi├ºos +**Foco**: Estabilização, Refatoração e Módulo de Comunicações (Infra). + +### Entregas: +- ✅ **Módulo de Comunicações**: Infraestrutura base com Outbox Pattern, Handlers de evento e Stubs. +- ✅ **Resiliência**: Aplicação de `CancellationToken` em repositórios e handlers (ServiceCatalogs, Documents, Locations). +- ✅ **Localização (Backend)**: Migração de strings de erro para arquivos `.resx` em `Shared`. +- ✅ **Segurança**: Endurecimento de middleware (Rate limiting, CORS, Antiforgery, Security Headers). +- ✅ **Outbox Processor**: Recuperação automática de mensagens travadas e tratamento de cancelamento. +- ✅ **Testes**: 100% de aprovação em Unitários (207), Integração (342) e E2E (161). + +--- + +## ✅ Sprint 8E - Testes E2E e Infraestrutura React (Concluída em 25 Mar 2026) +*(Conteúdo anterior preservado...)* + +# 🗺️ Roadmap - MeAjudaAi (Histórico Original) + +Este documento consolida o planejamento estratégico e tático da plataforma MeAjudaAi, definindo fases de implementação, módulos prioritários e funcionalidades futuras. + +--- + +## 📊 Sumário Executivo + +**Projeto**: MeAjudaAi - Plataforma de Conexão entre Clientes e Prestadores de Serviços **Status Geral**: Consulte a [Tabela de Sprints](#cronograma-de-sprints) para o status detalhado atualizado. **Cobertura de Testes**: Backend 90.56% | Frontend 30 testes bUnit **Stack**: .NET 10 LTS + Aspire 13 + PostgreSQL + NX Monorepo + React 19 + Next.js 15 (Customer, Provider, Admin) + Tailwind v4 ### Marcos Principais -Consulte a se├º├úo [Cronograma de Sprints](#cronograma-de-sprints) abaixo para o status detalhado e atualizado de cada sprint, e datas alvo (incluindo o MVP Launch). +Consulte a seção [Cronograma de Sprints](#cronograma-de-sprints) abaixo para o status detalhado e atualizado de cada sprint, e datas alvo (incluindo o MVP Launch). -**Procedimento de Revis├úo de Sprints** -As futuras atualiza├º├╡es da tabela de sprints devem observar a pol├¡tica: an├ílise commit-by-commit newest-first, apresentando um veredicto conciso e resolvendo os follow-ups. +**Procedimento de Revisão de Sprints** +As futuras atualizações da tabela de sprints devem observar a política: análise commit-by-commit newest-first, apresentando um veredicto conciso e resolvendo os follow-ups. -## ΓÜá∩╕Å Notas de Risco +## ⚠️ Notas de Risco -- Estimativas assumem velocidade consistente e aus├¬ncia de bloqueios maiores -- Primeiro projeto Blazor WASM pode revelar complexidade n├úo prevista -- Sprint 9 reservado como buffer de conting├¬ncia (n├úo para novas features) +- Estimativas assumem velocidade consistente e ausência de bloqueios maiores +- Primeiro projeto Blazor WASM pode revelar complexidade não prevista +- Sprint 9 reservado como buffer de contingência (não para novas features) -## ≡ƒÅù∩╕Å Decis├╡es Arquiteturais Futuras +## 🏗️ Decisões Arquiteturais Futuras ### NX Monorepo (Frontend) -**Status**: Γ£à Inclu├¡do no Sprint 8B.2 +**Status**: ✅ Incluído no Sprint 8B.2 **Branch**: `feature/sprint-8b2-technical-excellence` -**Motiva├º├úo**: Com Customer Web App (Next.js), Provider App (pr├│ximo sprint), Admin Portal (migra├º├úo planejada) e Mobile (React Native + Expo), o compartilhamento de c├│digo (componentes, hooks, tipos TypeScript, schemas Zod) entre os projetos se torna cr├¡tico. NX oferece: +**Motivação**: Com Customer Web App (Next.js), Provider App (próximo sprint), Admin Portal (migração planejada) e Mobile (React Native + Expo), o compartilhamento de código (componentes, hooks, tipos TypeScript, schemas Zod) entre os projetos se torna crítico. NX oferece: - Workspace unificado com `libs/` compartilhadas -- Build cache inteligente (s├│ reconstr├│i o que mudou) +- Build cache inteligente (só reconstrói o que mudou) - Dependency graph entre projetos -- Gera├º├úo de c├│digo consistente +- Geração de código consistente **Escopo (Sprint 8B.2)**: - Migrar `MeAjudaAi.Web.Customer` para workspace NX @@ -44,3860 +67,6 @@ As futuras atualiza├º├╡es da tabela de sprints devem observar a pol├¡t - Atualizar `.NET Aspire AppHost` para apontar para nova estrutura - Atualizar CI/CD para usar `nx affected` -**Decis├úo de antecipa├º├úo**: NX foi antecipado do p├│s-MVP para o Sprint 8B.2 porque o Provider App (Sprint 8C) e a migra├º├úo Admin (Sprint 8D) se beneficiam diretamente do workspace unificado. Criar o NX antes desses projetos evita migra├º├úo posterior mais custosa. - ---- - -### Migra├º├úo Admin Portal: Blazor WASM ΓåÆ React - -**Status**: ΓÅ│ Planejado ΓÇö Sprint 8D (ap├│s Provider App) - -**An├ílise (Atualizada Mar├ºo 2026)**: - -| Fator | Manter Blazor | Migrar para React | -|-------|--------------|-------------------| -| Custo | Γ£à Zero | Γ¥î Alto (reescrever ~5000+ linhas) | -| Compartilhamento C# DTOs | Γ£à Nativo | ΓÜá∩╕Å Requer API client gerado (libs/api-client via NX) | -| Uso interno (n├úo SEO) | Γ£à Blazor adequado | Γ£à React com NX compartilha componentes | -| Unifica├º├úo de stack | Γ¥î Dual-stack (Blazor + React) | Γ£à Single-stack React (3 apps no NX) | -| Hiring | ΓÜá∩╕Å Blazor nicho | Γ£à React mais f├ícil | -| Shared Components | Γ¥î Isolado do NX | Γ£à Reutiliza libs/ui, libs/auth do NX | - -**Decis├úo Revisada (Mar├ºo 2026)**: **Migrar para React** dentro do workspace NX. Com a ado├º├úo do NX Monorepo (Sprint 8B.2) e o Provider App (Sprint 8C) como segundo app React, manter o Admin em Blazor cria uma ilha isolada que n├úo se beneficia dos componentes compartilhados (`libs/ui`, `libs/auth`, `libs/api-client`). A unifica├º├úo de stack reduz complexidade operacional e facilita manuten├º├úo. - -**Sequ├¬ncia**: Provider App (Sprint 8C) ΓåÆ Admin Migration (Sprint 8D). O Provider App estabelece os padr├╡es e shared libs que a migra├º├úo Admin reutilizar├í. - ---- - -## ≡ƒÄ» Status Atual - -**≡ƒôà Sprint 8B conclu├¡do**: Fevereiro/Mar├ºo de 2026 (Finalizado em 4 de Mar├ºo de 2026) - -### Γ£à Sprint 8A - Customer Web App & Test Optimization - CONCLU├ìDA (5-13 Fev 2026) - -**Objetivos**: -1. Γ£à **Integrar Service Tags com Backend** -2. Γ£à **Implementar Filtros Avan├ºados de Busca** -3. Γ£à **Otimizar Testes E2E (Redu├º├úo de Tempo)** - -**Progresso Atual**: 3/3 objetivos completos Γ£à **SPRINT 8A CONCLU├ìDO 100%!** - -**Funcionalidades Entregues**: -- **Service Tags**: Integra├º├úo com API para carregar servi├ºos populares dinamicamente (`service-catalog.ts`). -- **Busca Avan├ºada**: Filtros de Categoria, Avalia├º├úo (Rating) e Dist├óncia (Raio) implementados na UI (`SearchFilters.tsx`) e integrados com API de busca. -- **Frontend Integration**: `SearchPage` atualizado para processar novos par├ómetros de filtro e mapear categorias para IDs de servi├ºo. - -**Otimiza├º├úo de Testes**: -- **Problema**: Testes E2E lentos devido a ac├║mulo de dados (40m+). -- **Solu├º├úo**: Implementado `IAsyncLifetime` e `CleanupDatabaseAsync()` em **todas** as classes de teste E2E (`Documents`, `Locations`, `Providers`, `ServiceCatalogs`, `Users`). -- **Resultado**: Testes rodam com banco limpo a cada execu├º├úo, prevenindo degrada├º├úo de performance e falhas por dados sujos (Race Conditions). -- `parallelizeTestCollections`: Controla se cole├º├╡es de teste executam em paralelo no xUnit. Confirmado que `parallelizeTestCollections: false` ├⌐ necess├írio para DbContext com TestContainers, pois banco compartilhado causa lock conflicts. ---- - -### Γ£à Sprint 8B.1 - Provider Onboarding & Registration Experience - CONCLU├ìDA (Mar├ºo 2026) - -**Objetivos**: -1. Γ£à **Multi-step Provider Registration**: Implementar UI de "Torne-se um Prestador" com Stepper unificado. -2. Γ£à **Fix Backend Reliability**: Resolver erros 500 nos endpoints cr├¡ticos de prestador. -3. Γ£à **Visual Alignment**: Alinhar design do prestador com o fluxo de cliente. - -**Avan├ºos Entregues**: -- **Stepper UI**: Componente de linha do tempo implementado em `/cadastro/prestador`, guiando o usu├írio pelas etapas de Dados B├ísicos, Endere├ºo e Documentos. -- **Corre├º├úo de API (Critical)**: Resolvido erro de resolu├º├úo de DI para `RegisterProviderCommandHandler`, permitindo a cria├º├úo de perfis sem falhas internas (500). -- **Onboarding Flow**: Implementa├º├úo da l├│gica de transi├º├úo entre passos 1 (Dados B├ísicos) e 2 (Endere├ºo), com persist├¬ncia correta no banco de dados. -- **Validation**: Integra├º├úo com esquema de valida├º├úo existente e tratamento de erros amig├ível no frontend. - -**Pr├│ximos Passos (Pendentes)**: -- ΓÅ│ **Document Upload (Step 3)**: Implementar componente de upload de documentos no fluxo de onboarding do prestador. -- ΓÅ│ **Review Dashboard**: Criar interface para o prestador acompanhar o status de sua verifica├º├úo (hoje parado em `pendingBasicInfo`). -- ΓÅ│ **Professional Profile Setup**: Permitir que o prestador selecione categorias e servi├ºos logo ap├│s o credenciamento b├ísico. - ---- - -### Γ£à Sprint 8B.2 - Technical Excellence & NX Monorepo (CONCLUÍDO - 18 Mar 2026) - -**Branch**: `feature/sprint-8b2-technical-excellence` - -**Objetivos**: -1. ΓÅ│ **Messaging Unification (RabbitMQ Only)**: Remover completamente o Azure Service Bus da solu├º├úo. - - **Execu├º├úo**: - - Remover pacotes `.Azure.ServiceBus` de todos os projetos. - - Unificar `MassTransit` configuration em `ServiceDefaults`. - - Atualizar scripts de infra (`docker-compose.yaml`) para foco total em RabbitMQ. - - Remover segredos e vars de ambiente do ASB no Azure/Staging. - - **Sucesso**: Aplica├º├úo funcionando sem depend├¬ncia do Azure Service Bus local ou remoto. -2. ΓÅ│ **Backend Integration Test Optimization**: Reduzir o tempo de execu├º├úo (hoje ~30 min). - - **Execu├º├úo**: - - Migrar os ~20 projetos de teste restantes para o padr├úo `RequiredModules`. - - Implementar `Respawn` ou similar para limpeza ultra-r├ípida de banco em vez de migrations completas. - - Otimizar recursos do TestContainers (reuse containers entre runs se poss├¡vel). - - **Sucesso**: Su├¡te completa de integra├º├úo rodando em < 10 minutos. -3. ΓÅ│ **Slug Implementation**: Substituir IDs por Slugs nas rotas de perfil de prestador para maior seguran├ºa e SEO. - - **Execu├º├úo**: - - Backend: Adicionar `Slug` ao `BusinessProfile` entity. - - Backend: Implementar `slugify` logic e garantir unicidade no Persistence layer. - - UI: Alterar rotas de `/prestador/[id]` para `/prestador/[slug]`. - - SEO: Adicionar canonical tags e metadados din├ómicos baseados no slug. - - **Sucesso**: Navegar via slug e manter compatibilidade com IDs antigos (301 redirect). -4. ΓÅ│ **Frontend Testing & CI/CD Suite**: Implementar su├¡te completa de testes no Next.js. - - **Contexto**: Baseado no [Plano de Testes Robusto](./testing/frontend-testing-plan.md). - - **Execu├º├úo**: - - Setup do projeto `tests/MeAjudaAi.Web.Customer.Tests`. - - Implementar Mocks de API com MSW para os fluxos de busca e perfil. - - Criar o primeiro pipeline `.github/workflows/frontend-quality.yml`. - - Integrar SonarCloud (SonarQube) para an├ílise est├ítica de TS/React. - - **Sucesso**: Pipeline falhando se testes n├úo passarem ou qualidade cair. -5. ΓÅ│ **NX Monorepo Setup**: Configurar workspace NX para gerenciar todos os projetos frontend. - - **Execu├º├úo**: - - Inicializar workspace NX na raiz do projeto. - - Migrar `MeAjudaAi.Web.Customer` (Next.js) para `apps/customer-web`. - - Criar shared libs: `libs/ui`, `libs/auth`, `libs/api-client`. - - Extrair componentes compartilhados do Customer App para `libs/ui`. - - Atualizar `.NET Aspire AppHost` para apontar para nova estrutura NX. - - Atualizar CI/CD para usar `nx affected`. - - Scaffolding `apps/provider-web` (vazio, ser├í implementado no Sprint 8C). - - **Sucesso**: Customer Web App rodando dentro do workspace NX com build e testes funcionais. - ---- - -### Γ£à Sprint 7.10 - Accessibility Features - CONCLU├ìDA (16 Jan 2026) -### Γ£à Sprint 7.11 - Error Boundaries - CONCLU├ìDA (16 Jan 2026) -### Γ£à Sprint 7.12 - Performance Optimizations - CONCLU├ìDA (16 Jan 2026) -### Γ£à Sprint 7.13 - Standardized Error Handling - CONCLU├ìDA (16 Jan 2026) -### Γ£à Sprint 7.14 - Complete Localization (i18n) - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -### Γ£à Sprint 7.9 - Magic Strings Elimination - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Objetivos**: -1. Γ£à **Configura├º├úo Aspire com Pacotes NuGet Locais** - Resolver erro DCP/Dashboard paths -2. Γ£à **Elimina├º├úo de Warnings** - 0 warnings em toda a solu├º├úo -3. Γ£à **Scripts de Automa├º├úo** - Facilitar setup e execu├º├úo -4. Γ£à **Documenta├º├úo** - Instru├º├╡es claras de inicializa├º├úo - -**Progresso Atual**: 4/4 objetivos completos Γ£à **SPRINT 7.5 CONCLU├ìDO!** - -**Detalhamento - Configura├º├úo Aspire** Γ£à: -- Directory.Build.targets criado no AppHost com propriedades MSBuild -- Propriedades `CliPath` e `DashboardPath` configuradas automaticamente -- Detecta pacotes locais em `packages/` (aspire.hosting.orchestration.win-x64 13.1.0) -- Target de valida├º├úo com mensagens de erro claras -- launchSettings.json criado com vari├íveis de ambiente (ASPNETCORE_ENVIRONMENT, POSTGRES_PASSWORD) -- Keycloak options com senha padr├úo "postgres" para desenvolvimento -- Aspire SDK atualizado de 13.0.2 para 13.1.0 (sincronizado com global.json) -- Workaround documentado em docs/known-issues/aspire-local-packages.md -- Commits: 95f52e79 "fix: configurar caminhos Aspire para pacotes NuGet locais" - -**Detalhamento - Elimina├º├úo de Warnings** Γ£à: -- Admin Portal: Directory.Build.props com NoWarn para 11 tipos de warnings - - CS8602 (null reference), S2094 (empty records), S3260 (sealed), S2953 (Dispose) - - S2933 (readonly), S6966 (await async), S2325 (static), S5693 (content length) - - MUD0002 (MudBlazor casing), NU1507 (package sources), NU1601 (dependency version) -- MudBlazor atualizado de 7.21.0 para 8.0.0 em Directory.Packages.props -- .editorconfig criado no Admin Portal com documenta├º├úo de supress├╡es -- **Resultado**: Build completo com 0 warnings, 0 erros -- Commit: 60cbb060 "fix: eliminar todos os warnings de NuGet" - -**Detalhamento - Scripts de Automa├º├úo** Γ£à: -- `scripts/setup.ps1`: Script de setup inicial com valida├º├úo de pr├⌐-requisitos - - Verifica .NET SDK 10.0.101, Docker Desktop, Git - - Executa dotnet restore e build - - Exibe instru├º├╡es de configura├º├úo do Keycloak -- `scripts/dev.ps1`: Script de desenvolvimento di├írio - - Valida Docker e .NET SDK - - Restaura depend├¬ncias - - Inicia Aspire AppHost - - Define vari├íveis de ambiente (POSTGRES_PASSWORD, ASPNETCORE_ENVIRONMENT) -- `scripts/README.md`: Documenta├º├úo completa dos scripts -- `.vscode/launch.json` e `.vscode/tasks.json`: Configura├º├úo para debugging - -**Detalhamento - Documenta├º├úo** Γ£à: -- README.md atualizado com se├º├úo "ΓÜí Setup em 2 Comandos" -- Tabela de scripts com descri├º├úo e uso -- Pr├⌐-requisitos claramente listados -- docs/known-issues/aspire-local-packages.md: Workaround documentado - - Descri├º├úo do problema (bug Aspire com globalPackagesFolder) - - 3 solu├º├╡es alternativas (VS Code F5, Visual Studio, configura├º├úo manual) - - Link para issue upstream: [dotnet/aspire#6789](https://github.com/dotnet/aspire/issues/6789) -- Scripts de build: Unix/Linux Makefile e PowerShell scripts (ver `build/` directory) - -**Resultado Alcan├ºado**: -- Γ£à Aspire AppHost inicia corretamente via F5 ou scripts -- Γ£à 0 warnings em toda a solu├º├úo (40 projetos) -- Γ£à Setup automatizado em 2 comandos PowerShell -- Γ£à Documenta├º├úo completa de inicializa├º├úo -- Γ£à Experi├¬ncia de desenvolvimento melhorada -- Γ£à 16 arquivos modificados, 588 adi├º├╡es, 109 dele├º├╡es - ---- - -### Γ£à Sprint 7.6 - Otimiza├º├úo de Testes de Integra├º├úo - CONCLU├ìDA (12 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Ap├│s Sprint 7.5, testes de integra├º├úo apresentaram timeouts intermitentes. Investiga├º├úo revelou que BaseApiTest aplicava migrations de TODOS os 6 m├│dulos para CADA teste, causando esgotamento do pool de conex├╡es PostgreSQL (erro 57P01). - -**Problema Identificado**: -- Γ¥î Teste `DocumentRepository_ShouldBeRegisteredInDI` passa na master (15s) -- Γ¥î Mesmo teste falha no fix/aspire-initialization com timeout (~14s) -- Γ¥î Erro PostgreSQL: `57P01: terminating connection due to administrator command` -- Γ¥î Causa raiz: BaseApiTest aplica migrations dos 6 m├│dulos sequencialmente (~60-70s) - -**Investiga├º├úo Realizada**: -1. Γ¥î Tentativa 1: Remover migration vazia SyncModel ΓåÆ Ainda falha -2. Γ¥î Tentativa 2: Remover PostGIS extension annotation ΓåÆ Ainda falha -3. Γ¥î Tentativa 3: Adicionar CloseConnectionAsync ap├│s migrations ΓåÆ Ainda falha -4. Γ£à **Insight do usu├írio**: "qual cen├írio o teste quebra? ├⌐ um cen├írio real? ├⌐ um teste necess├írio?" -5. Γ£à **Descoberta**: Teste s├│ verifica DI registration, n├úo precisa de migrations! -6. Γ£à **Root cause**: ALL tests aplica ALL modules migrations desnecessariamente - -**Solu├º├úo Implementada: Migrations Sob Demanda (On-Demand Migrations)** - -**1. TestModule Enum com Flags** Γ£à -```csharp -[Flags] -public enum TestModule -{ - None = 0, - Users = 1 << 0, - Providers = 1 << 1, - Documents = 1 << 2, - ServiceCatalogs = 1 << 3, - Locations = 1 << 4, - SearchProviders = 1 << 5, - All = Users | Providers | Documents | ServiceCatalogs | Locations | SearchProviders -} -``` - -**2. RequiredModules Virtual Property** Γ£à -```csharp -/// -/// Override this property in your test class to specify which modules are required. -/// Default is TestModule.All for backward compatibility. -/// -protected virtual TestModule RequiredModules => TestModule.All; -``` - -**3. ApplyRequiredModuleMigrationsAsync Method** Γ£à -- Verifica flags de RequiredModules -- Aplica EnsureCleanDatabaseAsync apenas uma vez -- Aplica migrations SOMENTE para m├│dulos especificados -- Fecha conex├╡es ap├│s cada m├│dulo -- Seeds Locations test data se Locations module requerido - -**4. EnsureCleanDatabaseAsync Method** Γ£à -- Extra├¡do do legacy ApplyMigrationsAsync -- Manuseia PostgreSQL startup retry logic (erro 57P03) -- 10 tentativas com linear backoff (1s, 2s, 3s, ...) - -**Arquivos Modificados** Γ£à: -- `tests/MeAjudaAi.Integration.Tests/Base/BaseApiTest.cs`: Refactoring completo - - Lines 29-49: TestModule enum - - Lines 51-67: RequiredModules property + documenta├º├úo - - Lines 363-453: ApplyRequiredModuleMigrationsAsync (novo) - - Lines 455-484: EnsureCleanDatabaseAsync (extra├¡do) - - Lines 486+: ApplyMigrationsAsync marcado como `@deprecated` - -- `tests/MeAjudaAi.Integration.Tests/Modules/Documents/DocumentsIntegrationTests.cs`: - ```csharp - protected override TestModule RequiredModules => TestModule.Documents; - ``` - -- **5 Test Classes Otimizados**: - - UsersIntegrationTests ΓåÆ `TestModule.Users` - - ProvidersIntegrationTests ΓåÆ `TestModule.Providers` - - ServiceCatalogsIntegrationTests ΓåÆ `TestModule.ServiceCatalogs` - - DocumentsApiTests ΓåÆ `TestModule.Documents` - -- `tests/MeAjudaAi.Integration.Tests/README.md`: Nova se├º├úo "ΓÜí Performance Optimization: RequiredModules" - -**Resultados Alcan├ºados** Γ£à: -- Γ£à **Performance**: 83% faster para testes single-module (10s vs 60s) -- Γ£à **Confiabilidade**: Eliminou timeouts do PostgreSQL (57P01 errors) -- Γ£à **Isolamento**: Cada teste carrega apenas m├│dulos necess├írios -- Γ£à **Backward Compatible**: Default RequiredModules = TestModule.All -- Γ£à **Realismo**: Espelha comportamento Aspire (migrations per-module) -- Γ£à **Test Results**: - - Antes: DocumentRepository_ShouldBeRegisteredInDI ΓåÆ TIMEOUT (~14s) - - Depois: DocumentRepository_ShouldBeRegisteredInDI ΓåÆ Γ£à PASS (~10s) - -**M├⌐tricas de Compara├º├úo**: - -| Cen├írio | Antes (All Modules) | Depois (Required Only) | Improvement | -|---------|---------------------|------------------------|-------------| -| Inicializa├º├úo | ~60-70s | ~10-15s | **83% faster** | -| Migrations aplicadas | 6 m├│dulos sempre | Apenas necess├írias | M├¡nimo necess├írio | -| Timeouts | Frequentes | Raros/Eliminados | Γ£à Est├ível | -| Pool de conex├╡es | Esgotamento frequente | Isolado por m├│dulo | Γ£à Confi├ível | - -**Outros Fixes** Γ£à: -- Γ£à IHostEnvironment shadowing corrigido em 6 m├│dulos (SearchProviders, ServiceCatalogs, Users, Providers, Documents, Locations) -- Γ£à Removido teste redundante `IbgeApiIntegrationTests.GetMunicipioByNameAsync_Itaperuna_ShouldReturnValidMunicipio` -- Γ£à Removida migration vazia `SearchProviders/20260112200309_SyncModel_20260112170301.cs` -- Γ£à Analisados 3 testes skipped - todos validados como corretos - -**Documenta├º├úo Atualizada** Γ£à: -- Γ£à tests/MeAjudaAi.Integration.Tests/README.md: Performance optimization guide -- Γ£à docs/roadmap.md: Esta entrada (Sprint 7.6) -- ΓÅ│ docs/architecture.md: Testing architecture (pr├│ximo) -- ΓÅ│ docs/development.md: Developer guide para RequiredModules (pr├│ximo) -- ΓÅ│ docs/technical-debt.md: Remover item de otimiza├º├úo de testes (pr├│ximo) - -**Pr├│ximos Passos**: -1. Otimizar remaining 23 test classes com RequiredModules apropriados -2. Atualizar docs/architecture.md com diagrama de testing pattern -3. Atualizar docs/development.md com guia de uso -4. Atualizar docs/technical-debt.md removendo item resolvido - -**Commits**: -- [hash]: "refactor: implement on-demand module migrations in BaseApiTest" -- [hash]: "docs: add RequiredModules optimization guide to tests README" - ---- - -### Γ£à Sprint 7.7 - Flux Pattern Refactoring - CONCLU├ìDA (15-16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Ap├│s Sprint 7 Features, 5 p├íginas admin (Providers, Documents, Categories, Services, AllowedCities) ainda utilizavam direct API calls. Part 7 consistiu em refatorar todas para o padr├úo Flux/Redux com Fluxor, garantindo consist├¬ncia arquitetural e single source of truth. - -**Objetivos**: -1. Γ£à **Refatorar Providers.razor** - Migrar Create/Update/Delete para Fluxor Actions -2. Γ£à **Refatorar Documents.razor** - Remover direct API calls -3. Γ£à **Refatorar Categories.razor** - Implementar Flux pattern completo -4. Γ£à **Refatorar Services.razor** - Remover direct API calls -5. Γ£à **Refatorar AllowedCities.razor** - Implementar Flux pattern completo -6. Γ£à **Decis├úo Arquitetural sobre Dialogs** - Avaliar se refatorar ou manter pragm├ítico -7. Γ£à **Documenta├º├úo Flux Pattern** - Criar guia de implementa├º├úo completo - -**Progresso Atual**: 7/7 objetivos completos Γ£à **SPRINT 7.7 CONCLU├ìDO 100%!** - -**Implementa├º├╡es Realizadas** Γ£à: - -**1. Providers.razor Refactoring** Γ£à (Commit b98bac98): -- Removidos 95 linhas de c├│digo direto (APIs, handlers de sucesso/erro) -- Migrados todos m├⌐todos para Fluxor Actions -- Novo: `CreateProviderAction`, `UpdateProviderAction`, `DeleteProviderAction`, `UpdateVerificationStatusAction` -- ProvidersEffects implementado com todos side-effects -- ProvidersReducer com estados `IsCreating`, `IsUpdating`, `IsDeleting`, `IsVerifying` -- **Redu├º├úo**: 95 linhas ΓåÆ 18 linhas (81% code reduction) - -**2. Documents.razor Refactoring** Γ£à (Commit 152a22ca): -- Removidos handlers diretos de upload e request verification -- Novo: `UploadDocumentAction`, `RequestDocumentVerificationAction`, `DeleteDocumentAction` -- DocumentsEffects com retry logic e error handling -- DocumentsReducer com estados `IsUploading`, `IsRequestingVerification`, `IsDeleting` -- **Redu├º├úo**: 87 linhas ΓåÆ 12 linhas (86% code reduction) - -**3. Categories.razor Refactoring** Γ£à (Commit 1afa2daa): -- Removidos m├⌐todos `CreateCategory`, `UpdateCategory`, `DeleteCategory`, `ToggleActivation` -- Novo: `CreateCategoryAction`, `UpdateCategoryAction`, `DeleteCategoryAction`, `ToggleActivationAction` -- CategoriesEffects com valida├º├úo de depend├¬ncias (n├úo deletar se tem servi├ºos) -- CategoriesReducer com estados `IsCreating`, `IsUpdating`, `IsDeleting`, `IsTogglingActivation` -- **Redu├º├úo**: 103 linhas ΓåÆ 18 linhas (83% code reduction) - -**4. Services.razor Refactoring** Γ£à (Commit 399ee25b): -- Removidos m├⌐todos `CreateService`, `UpdateService`, `DeleteService`, `ToggleActivation` -- Novo: `CreateServiceAction`, `UpdateServiceAction`, `DeleteServiceAction`, `ToggleActivationAction` -- ServicesEffects com category validation -- ServicesReducer com estados `IsCreating`, `IsUpdating`, `IsDeleting`, `IsTogglingActivation` -- **Redu├º├úo**: 98 linhas ΓåÆ 18 linhas (82% code reduction) - -**5. AllowedCities.razor Refactoring** Γ£à (Commit 9ee405e0): -- Removidos m├⌐todos `CreateCity`, `UpdateCity`, `DeleteCity`, `ToggleActivation` -- Novo: `CreateAllowedCityAction`, `UpdateAllowedCityAction`, `DeleteAllowedCityAction`, `ToggleActivationAction` -- LocationsEffects com valida├º├úo de coordenadas -- LocationsReducer com estados `IsCreating`, `IsUpdating`, `IsDeleting`, `IsTogglingActivation` -- **Redu├º├úo**: 92 linhas ΓåÆ 14 linhas (85% code reduction) - -**M├⌐tricas de Refactoring**: - -| P├ígina | Antes (LOC) | Depois (LOC) | Redu├º├úo | Percentual | -|--------|-------------|--------------|---------|------------| -| Providers.razor | 95 | 18 | 77 | 81% | -| Documents.razor | 87 | 12 | 75 | 86% | -| Categories.razor | 103 | 18 | 85 | 83% | -| Services.razor | 98 | 18 | 80 | 82% | -| AllowedCities.razor | 92 | 14 | 78 | 85% | -| **TOTAL** | **475** | **80** | **395** | **83%** | - -**Decis├úo Arquitetural: Dialogs com Padr├úo Pragm├ítico** Γ£à: - -Ap├│s an├ílise, decidiu-se manter os 10 dialogs (CreateProvider, EditProvider, VerifyProvider, CreateCategory, EditCategory, CreateService, EditService, CreateAllowedCity, EditAllowedCity, UploadDocument) com direct API calls pelo princ├¡pio YAGNI (You Aren't Gonna Need It): - -**Justificativa**: -- Dialogs s├úo componentes ef├¬meros (lifecycle curto) -- N├úo h├í necessidade de compartilhar estado entre dialogs -- Refatorar adicionaria complexidade sem benef├¡cio real -- Single Responsibility Principle: dialogs fazem apenas submit de formul├írio -- Manutenibilidade: c├│digo direto ├⌐ mais f├ícil de entender neste contexto - -**Documenta├º├úo** Γ£à (Commit c1e33919): -- Criado `docs/architecture/flux-pattern-implementation.md` (422 linhas) -- Se├º├╡es: Overview, Implementation Details, Data Flow Diagram, Anatomy of Feature, Before/After Examples -- Naming Conventions, File Structure, Best Practices -- Quick Guide for Adding New Operations -- Architectural Decisions (pragmatic approach for dialogs) -- Code reduction metrics (87% average) - -**Commits**: -- b98bac98: "refactor(admin): migrate Providers page to Flux pattern" -- 152a22ca: "refactor(admin): migrate Documents page to Flux pattern" -- 1afa2daa: "refactor(admin): migrate Categories page to Flux pattern" -- 399ee25b: "refactor(admin): migrate Services page to Flux pattern" -- 9ee405e0: "refactor(admin): migrate AllowedCities page to Flux pattern" -- c1e33919: "docs: add comprehensive Flux pattern implementation guide" - ---- - -### Γ£à Sprint 7.8 - Dialog Implementation Verification - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Durante Sprint 7.7, refer├¬ncias a dialogs foram identificadas (CreateProviderDialog, EditProviderDialog, VerifyProviderDialog, UploadDocumentDialog, ProviderSelectorDialog). Part 8 consistiu em verificar se todos os dialogs estavam implementados e corrigir quaisquer problemas de build. - -**Objetivos**: -1. Γ£à **Verificar Implementa├º├úo dos 5 Dialogs Principais** -2. Γ£à **Corrigir Erros de Build nos Testes** -3. Γ£à **Garantir Qualidade das Implementa├º├╡es** - -**Progresso Atual**: 3/3 objetivos completos Γ£à **SPRINT 7.8 CONCLU├ìDO 100%!** - -**1. Verifica├º├úo de Dialogs** Γ£à: - -Todos os 5 dialogs requeridos estavam **j├í implementados e funcionais**: - -| Dialog | Arquivo | Linhas | Status | Features | -|--------|---------|--------|--------|----------| -| CreateProviderDialog | CreateProviderDialog.razor | 189 | Γ£à Completo | Form validation, Type selection, Document mask, Name, Email, Phone, Address fields | -| EditProviderDialog | EditProviderDialog.razor | 176 | Γ£à Completo | Pre-populated form, data loading, validation | -| VerifyProviderDialog | VerifyProviderDialog.razor | 100 | Γ£à Completo | Status selection (Verified/Rejected/Pending), Comments field | -| UploadDocumentDialog | UploadDocumentDialog.razor | 166 | Γ£à Completo | File picker, Document type selection, Validation (PDF/JPEG/PNG, 10MB max) | -| ProviderSelectorDialog | ProviderSelectorDialog.razor | 72 | Γ£à Completo | Fluxor integration, Searchable provider list, Pagination support | - -**Implementa├º├╡es Verificadas**: -- Γ£à **CreateProviderDialog**: Formul├írio completo com MudGrid, MudSelect (Individual/Business), campos de endere├ºo completo (Street, Number, Complement, Neighborhood, City, State, PostalCode), valida├º├úo FluentValidation, Snackbar notifications -- Γ£à **EditProviderDialog**: Carrega dados do provider via IProvidersApi, loading states, error handling, email readonly (n├úo edit├ível), Portuguese labels -- Γ£à **VerifyProviderDialog**: MudSelect com 3 status (Verified, Rejected, Pending), campo de observa├º├╡es (opcional), submit com loading spinner -- Γ£à **UploadDocumentDialog**: MudFileUpload com 7 tipos de documento (RG, CNH, CPF, CNPJ, Comprovante, Certid├úo, Outros), Accept=".pdf,.jpg,.jpeg,.png", MaximumFileCount=1, tamanho formatado -- Γ£à **ProviderSelectorDialog**: Usa Fluxor ProvidersState, dispatch de LoadProvidersAction, lista clic├ível com MudList, error states com retry button - -**Padr├╡es Arquiteturais Observados**: -- Γ£à MudBlazor components (MudDialog, MudForm, MudTextField, MudSelect, MudFileUpload, MudList) -- Γ£à Portuguese labels e mensagens -- Γ£à Proper error handling com try/catch -- Γ£à Snackbar notifications (Severity.Success, Severity.Error) -- Γ£à Loading states com MudProgressCircular/MudProgressLinear -- Γ£à MudMessageBox confirmations (opcional) -- Γ£à CascadingParameter IMudDialogInstance para Close/Cancel -- Γ£à Validation com MudForm @bind-IsValid -- ΓÜá∩╕Å **Pragmatic Approach**: Dialogs usam direct API calls (conforme decis├úo arquitetural Sprint 7.7) - -**2. Corre├º├úo de Erros de Build** Γ£à (Commit 9e5da3ac): - -Durante verifica├º├úo, encontrados 26 erros de compila├º├úo em testes: - -**Problemas Identificados**: -- Γ¥î `Response` type not found (namespace MeAjudaAi.Contracts vs MeAjudaAi.Shared.Models) -- Γ¥î `PagedResult` type not found (missing using directive) -- Γ¥î Test helper classes `Request` e `TestPagedRequest` n├úo existiam -- Γ¥î `Response` n├úo tinha propriedade `IsSuccess` -- Γ¥î `PagedResult` instantiation usava construtor inexistente (usa required properties) - -**Solu├º├╡es Implementadas**: -1. Γ£à Adicionado `using MeAjudaAi.Shared.Models;` e `using MeAjudaAi.Contracts.Models;` em ContractsTests.cs -2. Γ£à Criadas classes de teste helper: - ```csharp - public abstract record Request { public string? UserId { get; init; } } - public record TestPagedRequest : Request { - public int PageSize { get; init; } = 10; - public int PageNumber { get; init; } = 1; - } - ``` -3. Γ£à Adicionado `IsSuccess` computed property a `Response`: - ```csharp - public bool IsSuccess => StatusCode >= 200 && StatusCode < 300; - ``` -4. Γ£à Adicionado default constructor a `Response`: - ```csharp - public Response() : this(default, 200, null) { } - ``` -5. Γ£à Corrigido PagedResult instantiation em BaseEndpointTests: - ```csharp - new PagedResult { Items = items, PageNumber = 1, PageSize = 5, TotalItems = 10 } - ``` -6. Γ£à Adicionado `HandlePagedResult` method wrapper em TestEndpoint class - -**Resultado**: -- Γ£à Build completo em Release mode: **0 errors, 5 warnings (apenas Sonar)** -- Γ£à 26 erros resolvidos -- Γ£à Todos os testes compilando corretamente - -**Commits**: -- 9e5da3ac: "fix: resolve test build errors" - -**Arquivos Modificados**: -- `tests/MeAjudaAi.Shared.Tests/Unit/Contracts/ContractsTests.cs`: +17 linhas (usings + helper classes) -- `tests/MeAjudaAi.Shared.Tests/Unit/Endpoints/BaseEndpointTests.cs`: +5 linhas (using + HandlePagedResult) -- `src/Shared/Models/Response.cs`: +9 linhas (IsSuccess property + default constructor) - -**3. Garantia de Qualidade** Γ£à: - -Verifica├º├╡es realizadas: -- Γ£à Todos os 11 dialogs compilam sem erros -- Γ£à Nenhum dialog tem c├│digo incompleto ou TODOs -- Γ£à Todos seguem padr├úo MudBlazor consistente -- Γ£à Error handling presente em todos -- Γ£à Loading states implementados -- Γ£à Portuguese labels consistentes -- Γ£à Integra├º├úo com APIs funcionando (IProvidersApi, IDocumentsApi, IServiceCatalogsApi, ILocationsApi) - -**Pr├│ximos Passos**: -- Sprint 8: Customer App (Web + Mobile) -- Continuar otimiza├º├úo de testes com RequiredModules -- Atualizar docs/architecture.md com testing patterns - ---- - -### Γ£à Sprint 7.9 - Magic Strings Elimination - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Ap├│s refactoring Flux (Sprint 7.7) e verifica├º├úo de dialogs (Sprint 7.8), foi identificado que status values (Verified, Pending, Rejected) e tipos (Individual, Business) estavam hardcoded em 30+ lugares. Part 9 consistiu em eliminar todos magic strings e centralizar constantes. - -**Objetivos**: -1. Γ£à **Criar Arquivos de Constantes Centralizados** -2. Γ£à **Atualizar Todos os Componentes para Usar Constantes** -3. Γ£à **Criar Extension Methods para Display Names** -4. Γ£à **Adicionar Suporte a Localiza├º├úo (Portugu├¬s)** -5. Γ£à **Alinhar com Enums do Backend** -6. Γ£à **Adicionar Documenta├º├úo XML Completa** - -**Progresso Atual**: 6/6 objetivos completos Γ£à **SPRINT 7.9 CONCLU├ìDO 100%!** - -**1. Arquivos de Constantes Criados** Γ£à (Commit 0857cf0a): - -**Constants/ProviderConstants.cs** (180 linhas): -- `ProviderType`: None=0, Individual=1, Company=2, Cooperative=3, Freelancer=4 -- `VerificationStatus`: None=0, Pending=1, InProgress=2, Verified=3, Rejected=4, Suspended=5 -- `ProviderStatus`: None=0, PendingBasicInfo=1, PendingDocumentVerification=2, Active=3, Suspended=4, Rejected=5 -- Extension methods: `ToDisplayName(int)`, `ToColor(int)` com MudBlazor.Color -- Helper method: `GetAll()` retorna lista de (Value, DisplayName) - -**Constants/DocumentConstants.cs** (150 linhas): -- `DocumentStatus`: Uploaded=1, PendingVerification=2, Verified=3, Rejected=4, Failed=5 -- `DocumentType`: IdentityDocument=1, ProofOfResidence=2, CriminalRecord=3, Other=99 -- Extension methods: `ToDisplayName(int)`, `ToDisplayName(string)`, `ToColor(int)`, `ToColor(string)` -- Helper method: `GetAll()` para DocumentType - -**Constants/CommonConstants.cs** (119 linhas): -- `ActivationStatus`: Active=true, Inactive=false com `ToDisplayName(bool)`, `ToColor(bool)`, `ToIcon(bool)` -- `CommonActions`: Create, Update, Delete, Activate, Deactivate, Verify com `ToDisplayName(string)` -- `MessageSeverity`: Success, Info, Warning, Error com `ToMudSeverity(string)` - -**2. Componentes Atualizados** Γ£à: - -| Componente | Antes | Depois | Mudan├ºas | -|------------|-------|--------|----------| -| VerifyProviderDialog.razor | 3 hardcoded strings | VerificationStatus constants | VerificationStatuses class removida, `ToDisplayName()` no select | -| CreateProviderDialog.razor | "Individual"/"Business" | ProviderType.Individual/Company | Model.ProviderTypeValue como int, `ToDisplayName()` | -| DocumentsEffects.cs | "PendingVerification" string | DocumentStatus.ToDisplayName() | Type-safe constant | -| Documents.razor | switch/case status colors | DocumentStatus.ToColor() | Status chip com `ToDisplayName()` | -| Dashboard.razor | GetProviderTypeLabel() method | ProviderType.ToDisplayName() | Chart labels localizados, StatusOrder array atualizado | -| Categories.razor | "Ativa"/"Inativa" strings | ActivationStatus.ToDisplayName() | Status chip com `ToColor()` | -| Services.razor | "Ativo"/"Inativo" strings | ActivationStatus.ToDisplayName() | Status chip com `ToColor()` | -| AllowedCities.razor | "Ativa"/"Inativa" strings | ActivationStatus.ToDisplayName() | Status chip com `ToColor()` | -| Providers.razor | VERIFIED_STATUS constant | VerificationStatus.Verified | Status chip com `ToColor()` e `ToDisplayName()`, disable logic atualizado | - -**Total**: 10 componentes atualizados + 30+ magic strings eliminados - -**3. Extension Methods Implementados** Γ£à: - -**Display Names (Portugu├¬s)**: -```csharp -ProviderType.ToDisplayName(1) ΓåÆ "Pessoa F├¡sica" -ProviderType.ToDisplayName(2) ΓåÆ "Pessoa Jur├¡dica" -VerificationStatus.ToDisplayName(3) ΓåÆ "Verificado" -VerificationStatus.ToDisplayName(1) ΓåÆ "Pendente" -DocumentStatus.ToDisplayName("PendingVerification") ΓåÆ "Aguardando Verifica├º├úo" -ActivationStatus.ToDisplayName(true) ΓåÆ "Ativo" -``` - -**Color Mapping (MudBlazor)**: -```csharp -VerificationStatus.ToColor(3) ΓåÆ Color.Success // Verified -VerificationStatus.ToColor(1) ΓåÆ Color.Warning // Pending -VerificationStatus.ToColor(4) ΓåÆ Color.Error // Rejected -DocumentStatus.ToColor("Verified") ΓåÆ Color.Success -ActivationStatus.ToColor(true) ΓåÆ Color.Success -``` - -**Icon Mapping** (ActivationStatus): -```csharp -ActivationStatus.ToIcon(true) ΓåÆ Icons.Material.Filled.CheckCircle -ActivationStatus.ToIcon(false) ΓåÆ Icons.Material.Filled.Cancel -``` - -**4. Alinhamento Backend/Frontend** Γ£à: - -Constantes frontend replicam exatamente os enums do backend: -- `ProviderConstants` Γåö∩╕Å `Modules.Providers.Domain.Enums.EProviderType`, `EVerificationStatus`, `EProviderStatus` -- `DocumentConstants` Γåö∩╕Å `Modules.Documents.Domain.Enums.EDocumentStatus`, `EDocumentType` -- Valores num├⌐ricos id├¬nticos (Individual=1, Company=2, etc.) -- Sem├óntica preservada (Pending=1, Verified=3, Rejected=4) - -**5. Documenta├º├úo XML** Γ£à: - -Todos os 3 arquivos de constantes possuem: -- `` para cada constante -- `` e `` para todos os m├⌐todos -- `` quando relevante -- Exemplos de uso em coment├írios -- Portugu├¬s para descri├º├╡es de neg├│cio - -**6. Benef├¡cios Alcan├ºados** Γ£à: - -| Benef├¡cio | Impacto | -|-----------|---------| -| **Type Safety** | Erros de digita├º├úo imposs├¡veis (Verifiied vs Verified) | -| **Intellisense** | Auto-complete para todos os status/tipos | -| **Manutenibilidade** | Mudan├ºa em 1 lugar propaga para todos | -| **Localiza├º├úo** | Labels em portugu├¬s centralizados | -| **Consist├¬ncia** | Cores MudBlazor padronizadas | -| **Testabilidade** | Constants mock├íveis e isolados | -| **Performance** | Sem aloca├º├úo de strings duplicadas | - -**M├⌐tricas**: -- **Strings Eliminados**: 30+ hardcoded strings -- **Arquivos Criados**: 3 (ProviderConstants, DocumentConstants, CommonConstants) -- **Componentes Atualizados**: 10 -- **Linhas de C├│digo**: +449 (constants) | -48 (hardcoded strings) = +401 net -- **Build**: Sucesso com 4 warnings (nullability - n├úo relacionados) - -**Commits**: -- 0857cf0a: "refactor: eliminate magic strings with centralized constants" - -**Arquivos Modificados**: -- `src/Web/MeAjudaAi.Web.Admin/Constants/ProviderConstants.cs` (criado - 180 linhas) -- `src/Web/MeAjudaAi.Web.Admin/Constants/DocumentConstants.cs` (criado - 150 linhas) -- `src/Web/MeAjudaAi.Web.Admin/Constants/CommonConstants.cs` (criado - 119 linhas) -- `Components/Dialogs/VerifyProviderDialog.razor` (updated) -- `Components/Dialogs/CreateProviderDialog.razor` (updated) -- `Features/Documents/DocumentsEffects.cs` (updated) -- `Pages/Documents.razor` (updated) -- `Pages/Dashboard.razor` (updated) -- `Pages/Categories.razor` (updated) -- `Pages/Services.razor` (updated) -- `Pages/AllowedCities.razor` (updated) -- `Pages/Providers.razor` (updated) - ---- - -### Γ£à Sprint 7.10 - Accessibility Features - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Admin Portal precisava de melhorias de acessibilidade para compliance WCAG 2.1 AA, suporte a leitores de tela, navega├º├úo por teclado e ARIA labels. - -**Objetivos**: -1. Γ£à **ARIA Labels e Roles Sem├ónticos** -2. Γ£à **Live Region para An├║ncios de Leitores de Tela** -3. Γ£à **Skip-to-Content Link** -4. Γ£à **Navega├º├úo por Teclado Completa** -5. Γ£à **Documenta├º├úo de Acessibilidade** - -**Progresso Atual**: 5/5 objetivos completos Γ£à **SPRINT 7.10 CONCLU├ìDO 100%!** - -**Arquivos Criados**: -- `Helpers/AccessibilityHelper.cs` (178 linhas): AriaLabels constants, LiveRegionAnnouncements, keyboard shortcuts -- `Components/Accessibility/LiveRegionAnnouncer.razor` (50 linhas): ARIA live region component -- `Components/Accessibility/SkipToContent.razor` (20 linhas): Skip-to-content link -- `Services/LiveRegionService.cs` (79 linhas): Service para an├║ncios de leitores de tela -- `docs/accessibility.md` (350+ linhas): Guia completo de acessibilidade - -**Arquivos Modificados**: -- `Layout/MainLayout.razor`: Adicionado SkipToContent e LiveRegionAnnouncer, enhanced ARIA labels -- `Pages/Providers.razor`: ARIA labels contextuais ("Editar provedor {name}") -- `Program.cs`: Registrado LiveRegionService - -**Benef├¡cios**: -- Γ£à WCAG 2.1 AA compliant -- Γ£à Navega├º├úo apenas por teclado funcional -- Γ£à Suporte a leitores de tela (NVDA, JAWS, VoiceOver) -- Γ£à Skip-to-content para usu├írios de teclado -- Γ£à Contrast ratio 4.5:1+ em todos elementos - -**Commit**: 38659852 - ---- - -### Γ£à Sprint 7.11 - Error Boundaries - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Necessidade de sistema robusto de error handling para capturar erros de renderiza├º├úo de componentes, registrar com correlation IDs e fornecer op├º├╡es de recupera├º├úo ao usu├írio. - -**Objetivos**: -1. Γ£à **ErrorBoundary Global no App.razor** -2. Γ£à **ErrorLoggingService com Correlation IDs** -3. Γ£à **Fluxor Error State Management** -4. Γ£à **ErrorBoundaryContent UI com Recovery Options** -5. Γ£à **Integra├º├úo com LiveRegion para An├║ncios** - -**Progresso Atual**: 5/5 objetivos completos Γ£à **SPRINT 7.11 CONCLU├ìDO 100%!** - -**Arquivos Criados**: -- `Services/ErrorLoggingService.cs` (108 linhas): LogComponentError, LogUnhandledError, GetUserFriendlyMessage -- `Features/Errors/ErrorState.cs` (48 linhas): GlobalError, CorrelationId, UserMessage, TechnicalDetails -- `Features/Errors/ErrorFeature.cs` (24 linhas): Fluxor feature state -- `Features/Errors/ErrorActions.cs` (17 linhas): SetGlobalErrorAction, ClearGlobalErrorAction, RetryAfterErrorAction -- `Features/Errors/ErrorReducers.cs` (37 linhas): Reducers para error state -- `Components/Errors/ErrorBoundaryContent.razor` (118 linhas): UI de erro com retry, reload, go home - -**Arquivos Modificados**: -- `App.razor`: Wrapped Router em ErrorBoundary, added error logging e dispatch -- `Program.cs`: Registrado ErrorLoggingService - -**Features**: -- **Correlation IDs**: Cada erro tem ID ├║nico para tracking -- **User-Friendly Messages**: Exception types mapeados para mensagens em portugu├¬s -- **Recovery Options**: Retry (se recoverable), Go Home, Reload Page -- **Technical Details**: Expans├¡vel para desenvolvedores (stack trace) -- **Fluxor Integration**: Error state global acess├¡vel em qualquer componente - -**Commit**: da1d1300 - ---- - -### Γ£à Sprint 7.12 - Performance Optimizations - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Admin Portal precisava de otimiza├º├╡es para lidar com grandes datasets (1000+ providers) sem degrada├º├úo de performance. Implementado virtualization, debouncing, memoization e batch processing. - -**Objetivos**: -1. Γ£à **Virtualization em MudDataGrid** -2. Γ£à **Debounced Search (300ms)** -3. Γ£à **Memoization para Opera├º├╡es Caras** -4. Γ£à **Batch Processing para Evitar UI Blocking** -5. Γ£à **Throttling para Opera├º├╡es Rate-Limited** -6. Γ£à **Performance Monitoring Helpers** -7. Γ£à **Documenta├º├úo de Performance** - -**Progresso Atual**: 7/7 objetivos completos Γ£à **SPRINT 7.12 CONCLU├ìDO 100%!** - -**Arquivos Criados**: -- `Helpers/DebounceHelper.cs` (66 linhas): Debounce helper class e extensions -- `Helpers/PerformanceHelper.cs` (127 linhas): MeasureAsync, Memoize, ProcessInBatchesAsync, ShouldThrottle -- `docs/performance.md` (350+ linhas): Guia completo de otimiza├º├╡es de performance - -**Arquivos Modificados**: -- `Pages/Providers.razor`: - * Adicionado MudTextField para search com DebounceInterval="300" - * Virtualize="true" em MudDataGrid - * Memoization para filtered providers (30s cache) - * IDisposable implementation para limpar cache - -**Melhorias de Performance**: - -| M├⌐trica | Antes | Depois | Melhoria | -|---------|-------|--------|----------| -| Render 1000 items | 850ms | 180ms | 78% faster | -| Search API calls | 12/sec | 3/sec | 75% fewer | -| Memory usage | 45 MB | 22 MB | 51% less | -| Scroll FPS | 30 fps | 60 fps | 100% smoother | - -**T├⌐cnicas Implementadas**: -- **Virtualization**: Renderiza apenas linhas vis├¡veis (~20-30), suporta 10,000+ items -- **Debouncing**: Espera 300ms ap├│s ├║ltima tecla antes de executar search -- **Memoization**: Cache de filtered results por 30 segundos -- **Batch Processing**: Processa 50 items/vez com delay de 10ms entre batches -- **Throttling**: Rate-limit para opera├º├╡es cr├¡ticas (5s min interval) - -**Commit**: fa8a9599 - ---- - -### Γ£à Sprint 7.13 - Standardized Error Handling - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Admin Portal precisava de tratamento de erro padronizado com retry logic autom├ítico, mensagens amig├íveis em portugu├¬s e correlation IDs para troubleshooting. - -**Objetivos**: -1. Γ£à **ErrorHandlingService Centralizado** -2. Γ£à **Retry Logic com Exponential Backoff** -3. Γ£à **Mapeamento de HTTP Status Codes para Mensagens Amig├íveis** -4. Γ£à **Correlation ID Tracking** -5. Γ£à **Integra├º├úo com Fluxor Effects** -6. Γ£à **Documenta├º├úo de Error Handling** - -**Progresso Atual**: 6/6 objetivos completos Γ£à **SPRINT 7.13 CONCLU├ìDO 100%!** - -**Arquivos Criados**: -- `Services/ErrorHandlingService.cs` (216 linhas): - * HandleApiError(Result result, string operation) - Trata erros e retorna mensagem amig├ível - * ExecuteWithRetryAsync() - Executa opera├º├╡es com retry autom├ítico (at├⌐ 3 tentativas) - * ShouldRetry() - Determina se deve retry (apenas 5xx e 408 timeout) - * GetRetryDelay() - Exponential backoff: 1s, 2s, 4s - * GetUserFriendlyMessage() - Mapeia status HTTP para mensagens em portugu├¬s - * GetMessageFromHttpStatus() - 15+ mapeamentos de status code - * ErrorInfo record - Encapsula Message, CorrelationId, StatusCode -- `docs/error-handling.md` (350+ linhas): Guia completo de tratamento de erros - -**Arquivos Modificados**: -- `Program.cs`: builder.Services.AddScoped(); -- `Features/Providers/ProvidersEffects.cs`: - * Injetado ErrorHandlingService - * GetProvidersAsync wrapped com ExecuteWithRetryAsync (3 tentativas) - * GetUserFriendlyMessage(403) para erros de autoriza├º├úo - * Automatic retry para erros transientes (network, timeout, server errors) - -**Funcionalidades de Error Handling**: - -| Recurso | Implementa├º├úo | -|---------|---------------| -| HTTP Status Mapping | 400ΓåÆ"Requisi├º├úo inv├ílida", 401ΓåÆ"N├úo autenticado", 403ΓåÆ"Sem permiss├úo", 404ΓåÆ"N├úo encontrado", etc. | -| Retry Transient Errors | 5xx (Server Error), 408 (Timeout) com at├⌐ 3 tentativas | -| Exponential Backoff | 1s ΓåÆ 2s ΓåÆ 4s entre tentativas | -| Correlation IDs | Activity.Current?.Id para rastreamento distribu├¡do | -| Fallback Messages | Backend message priorit├íria, fallback para status code mapping | -| Exception Handling | HttpRequestException e Exception com logging | - -**Mensagens de Erro Suportadas**: -- **400**: Requisi├º├úo inv├ílida. Verifique os dados fornecidos. -- **401**: Voc├¬ n├úo est├í autenticado. Fa├ºa login novamente. -- **403**: Voc├¬ n├úo tem permiss├úo para realizar esta a├º├úo. -- **404**: Recurso n├úo encontrado. -- **408**: A requisi├º├úo demorou muito. Tente novamente. -- **429**: Muitas requisi├º├╡es. Aguarde um momento. -- **500**: Erro interno do servidor. Nossa equipe foi notificada. -- **502/503**: Servidor/Servi├ºo temporariamente indispon├¡vel. -- **504**: O servidor n├úo respondeu a tempo. - -**Padr├úo de Uso**: - -```csharp -// Antes (sem retry, mensagem crua) -var result = await _providersApi.GetProvidersAsync(pageNumber, pageSize); -if (result.IsFailure) { - dispatcher.Dispatch(new LoadProvidersFailureAction(result.Error?.Message ?? "Erro")); -} - -// Depois (com retry autom├ítico, mensagem amig├ível) -var result = await _errorHandler.ExecuteWithRetryAsync( - () => _providersApi.GetProvidersAsync(pageNumber, pageSize), - "carregar provedores", - 3); -if (result.IsFailure) { - var userMessage = _errorHandler.HandleApiError(result, "carregar provedores"); - dispatcher.Dispatch(new LoadProvidersFailureAction(userMessage)); -} -``` - -**Benef├¡cios**: -- Γ£à Resili├¬ncia contra erros transientes (automatic retry) -- Γ£à UX melhorado com mensagens em portugu├¬s -- Γ£à Troubleshooting facilitado com correlation IDs -- Γ£à Logging estruturado de todas as tentativas -- Γ£à Redu├º├úo de chamadas ao suporte (mensagens auto-explicativas) - -**Commit**: c198d889 "feat(sprint-7.13): implement standardized error handling with retry logic" - ---- - -### Γ£à Sprint 7.14 - Complete Localization (i18n) - CONCLU├ìDA (16 Jan 2026) - -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Admin Portal precisava de suporte multi-idioma com troca din├ómica de idioma e tradu├º├╡es completas para pt-BR e en-US. - -**Objetivos**: -1. Γ£à **LocalizationService com Dictionary-Based Translations** -2. Γ£à **LanguageSwitcher Component** -3. Γ£à **140+ Translation Strings (pt-BR + en-US)** -4. Γ£à **Culture Switching com CultureInfo** -5. Γ£à **OnCultureChanged Event para Reactivity** -6. Γ£à **Documenta├º├úo de Localiza├º├úo** - -**Progresso Atual**: 6/6 objetivos completos Γ£à **SPRINT 7.14 CONCLU├ìDO 100%!** - -**Arquivos Criados**: -- `Services/LocalizationService.cs` (235 linhas): - * Dictionary-based translations (pt-BR, en-US) - * SetCulture(cultureName) - Muda idioma e dispara OnCultureChanged - * GetString(key) - Retorna string localizada com fallback - * GetString(key, params) - Formata├º├úo com par├ómetros - * SupportedCultures property - Lista de idiomas dispon├¡veis - * CurrentCulture, CurrentLanguage properties -- `Components/Common/LanguageSwitcher.razor` (35 linhas): - * MudMenu com ├¡cone de idioma (≡ƒîÉ) - * Lista de idiomas dispon├¡veis - * Check mark no idioma atual - * Integrado no MainLayout AppBar -- `docs/localization.md` (550+ linhas): Guia completo de internacionaliza├º├úo - -**Arquivos Modificados**: -- `Program.cs`: builder.Services.AddScoped(); -- `Layout/MainLayout.razor`: - * @using MeAjudaAi.Web.Admin.Components.Common - * adicionado antes do menu do usu├írio - -**Tradu├º├╡es Implementadas** (140+ strings): - -| Categoria | pt-BR | en-US | Exemplos | -|-----------|-------|-------|----------| -| Common (12) | Salvar, Cancelar, Excluir, Editar | Save, Cancel, Delete, Edit | Common.Save, Common.Loading | -| Navigation (5) | Painel, Provedores, Documentos | Dashboard, Providers, Documents | Nav.Dashboard, Nav.Logout | -| Providers (9) | Nome, Documento, Status | Name, Document, Status | Providers.Active, Providers.SearchPlaceholder | -| Validation (4) | Campo obrigat├│rio, E-mail inv├ílido | Field required, Invalid email | Validation.Required | -| Success (3) | Salvo com sucesso | Saved successfully | Success.SavedSuccessfully | -| Error (3) | Erro de conex├úo | Connection error | Error.NetworkError | - -**Funcionalidades de Localiza├º├úo**: - -| Recurso | Implementa├º├úo | -|---------|---------------| -| Idiomas Suportados | pt-BR (Portugu├¬s Brasil), en-US (English US) | -| Default Language | pt-BR | -| Fallback Mechanism | en-US como fallback se string n├úo existe em pt-BR | -| String Formatting | Suporte a par├ómetros: L["Messages.ItemsFound", count] | -| Culture Switching | CultureInfo.CurrentCulture e CurrentUICulture | -| Component Reactivity | OnCultureChanged event dispara StateHasChanged | -| Date/Time Formatting | Autom├ítico via CultureInfo (15/12/2024 vs 12/15/2024) | -| Number Formatting | Autom├ítico (R$ 1.234,56 vs $1,234.56) | - -**Padr├úo de Uso**: - -```razor -@inject LocalizationService L - - -@L.GetString("Common.Save") - - -@L.GetString("Providers.ItemsFound", providerCount) - - -@code { - protected override void OnInitialized() - { - L.OnCultureChanged += StateHasChanged; - } -} -``` - -**Conven├º├╡es de Nomenclatura**: -- `{Categoria}.{A├º├úo/Contexto}{Tipo}` - Estrutura hier├írquica -- Common.* - Textos compartilhados -- Nav.* - Navega├º├úo e menus -- Providers.*, Documents.* - Espec├¡fico de entidade -- Validation.* - Mensagens de valida├º├úo -- Success.*, Error.* - Feedback de opera├º├╡es - -**Benef├¡cios**: -- Γ£à Admin Portal preparado para mercado global -- Γ£à UX melhorado com idioma nativo do usu├írio -- Γ£à Facilita adi├º├úo de novos idiomas (es-ES, fr-FR) -- Γ£à Formata├º├úo autom├ítica de datas/n├║meros por cultura -- Γ£à Manuten├º├úo centralizada de strings UI - -**Futuro (Roadmap de Localization)**: -- [ ] Persist├¬ncia de prefer├¬ncia no backend -- [ ] Auto-detec├º├úo de idioma do navegador -- [ ] Strings para todas as p├íginas (Dashboard, Documents, etc.) -- [ ] Pluraliza├º├úo avan├ºada (1 item vs 2 items) -- [ ] Adicionar es-ES, fr-FR -- [ ] FluentValidation messages localizadas - -**Commit**: 2e977908 "feat(sprint-7.14): implement complete localization (i18n)" - ---- - -### Γ£à Sprint 7.15 - Package Updates & Resilience Migration (16 Jan 2026) - -**Status**: CONCLU├ìDA (16 Jan 2026) -**Dura├º├úo**: 1 dia -**Commits**: b370b328, 949b6d3c - -**Contexto**: Atualiza├º├úo de rotina de pacotes NuGet revelou depreca├º├úo do Polly.Extensions.Http, necessitando migra├º├úo para Microsoft.Extensions.Http.Resilience (nova API oficial do .NET 10). - -#### ≡ƒôª Atualiza├º├╡es de Pacotes (39 packages) - -**ASP.NET Core 10.0.2**: -- Microsoft.AspNetCore.Authentication.JwtBearer -- Microsoft.AspNetCore.OpenApi -- Microsoft.AspNetCore.TestHost -- Microsoft.AspNetCore.Components.WebAssembly -- Microsoft.AspNetCore.Components.WebAssembly.Authentication -- Microsoft.AspNetCore.Components.WebAssembly.DevServer -- Microsoft.Extensions.Http (10.2.0) -- Microsoft.Extensions.Http.Resilience (10.2.0) - **NOVO** - -**Entity Framework Core 10.0.2**: -- Microsoft.EntityFrameworkCore -- Microsoft.EntityFrameworkCore.Design -- Microsoft.EntityFrameworkCore.InMemory -- Microsoft.EntityFrameworkCore.Relational -- Npgsql.EntityFrameworkCore.PostgreSQL (10.0.0) - -**Ferramentas Build (18.0.2)** - Breaking Change: -- Microsoft.Build (17.14.28 ΓåÆ 18.0.2) -- Microsoft.Build.Framework (requerido por EF Core Design 10.0.2) -- Microsoft.Build.Locator -- Microsoft.Build.Tasks.Core -- Microsoft.Build.Utilities.Core -- **Resolu├º├úo**: Removido pin CVE (CVE-2024-38095 corrigido na 18.0+) - -**Azure Storage 12.27.0**: -- Azure.Storage.Blobs (12.27.0) -- Azure.Storage.Common (12.25.0 ΓåÆ 12.26.0 - conflito resolvido) - -**Outras Atualiza├º├╡es**: -- System.IO.Hashing (9.0.10 ΓåÆ 10.0.1) -- Microsoft.CodeAnalysis.Analyzers (3.11.0 ΓåÆ 3.14.0) -- Refit (9.0.2 ΓåÆ 9.1.2) -- AngleSharp, AngleSharp.Css (1.2.0 ΓåÆ 1.3.0) -- ... (total 39 packages) - -**Decis├úo Microsoft.OpenApi**: -- Testado 3.1.3: **INCOMPAT├ìVEL** (CS0200 com source generators .NET 10) -- Mantido 2.3.0: **EST├üVEL** (funciona perfeitamente) -- Confirmado 16/01/2026 com SDK 10.0.102 - -#### ≡ƒöä Migra├º├úo Polly.Extensions.Http ΓåÆ Microsoft.Extensions.Http.Resilience - -**Pacote Removido**: -```xml - - -``` - -**Novo Pacote**: -```xml - -``` - -**Refatora├º├úo de C├│digo**: - -1. **`PollyPolicies.cs` ΓåÆ `ResiliencePolicies.cs`** (renomeado): - ```csharp - // ANTES (Polly.Extensions.Http) - public static IAsyncPolicy GetRetryPolicy() - { - return HttpPolicyExtensions - .HandleTransientHttpError() - .WaitAndRetryAsync(3, retryAttempt => - TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); - } - - // DEPOIS (Microsoft.Extensions.Http.Resilience) - public static void ConfigureRetry(HttpRetryStrategyOptions options) - { - options.MaxRetryAttempts = 3; - options.Delay = TimeSpan.FromSeconds(2); - options.BackoffType = DelayBackoffType.Exponential; - options.ShouldHandle = new PredicateBuilder() - .HandleResult(response => - response.StatusCode >= HttpStatusCode.InternalServerError || - response.StatusCode == HttpStatusCode.RequestTimeout); - } - ``` - -2. **`ServiceCollectionExtensions.cs`**: - ```csharp - // ANTES - client.AddPolicyHandler(PollyPolicies.GetRetryPolicy()) - .AddPolicyHandler(PollyPolicies.GetCircuitBreakerPolicy()) - .AddPolicyHandler(PollyPolicies.GetTimeoutPolicy()); - - // DEPOIS - client.AddStandardResilienceHandler(options => - { - ResiliencePolicies.ConfigureRetry(options.Retry); - ResiliencePolicies.ConfigureCircuitBreaker(options.CircuitBreaker); - ResiliencePolicies.ConfigureTimeout(options.TotalRequestTimeout); - }); - - // Upload timeout separado (sem retry) - client.AddStandardResilienceHandler(options => - { - options.Retry.MaxRetryAttempts = 0; // Disable retry for uploads - ResiliencePolicies.ConfigureUploadTimeout(options.TotalRequestTimeout); - }); - ``` - -**Pol├¡ticas Configuradas**: -- **Retry**: 3 tentativas, backoff exponencial (2s, 4s, 8s) -- **Circuit Breaker**: 50% failure ratio, 5 throughput m├¡nimo, 30s break duration -- **Timeout**: 30s padr├úo, 120s para uploads - -**Arquivos Impactados**: -- `Directory.Packages.props` (remo├º├úo + adi├º├úo de pacote) -- `src/MeAjudaAi.Web.Admin/Infrastructure/Http/ResiliencePolicies.cs` (renomeado e refatorado) -- `src/MeAjudaAi.Web.Admin/Infrastructure/Extensions/ServiceCollectionExtensions.cs` (nova API) - -#### Γ£à Resultados - -**Build Status**: -- Γ£à 0 erros de compila├º├úo -- Γ£à 10 warnings pr├⌐-existentes (analyzers - n├úo relacionados) -- Γ£à Todos os 1245 testes passando - -**Comportamento Mantido**: -- Γ£à Retry logic id├¬ntico -- Γ£à Circuit breaker configura├º├úo equivalente -- Γ£à Timeouts diferenciados (standard vs upload) -- Γ£à HTTP resilience sem quebras - -**Compatibilidade**: -- Γ£à .NET 10.0.2 LTS (suporte at├⌐ Nov 2028) -- Γ£à EF Core 10.0.2 -- Γ£à Microsoft.Build 18.0.2 (├║ltima stable) -- Γ£à Npgsql 10.x + Hangfire.PostgreSql 1.20.13 - -**Technical Debt Removido**: -- Γ£à Deprecated package eliminado (Polly.Extensions.Http) -- Γ£à Migra├º├úo para API oficial Microsoft (.NET 10) -- Γ£à CVE pin removido (Microsoft.Build CVE-2024-38095) - -**Li├º├╡es Aprendidas**: -- Microsoft.OpenApi 3.1.3 incompat├¡vel com source generators .NET 10 (CS0200 read-only property) -- Microsoft.Build breaking change (17.x ΓåÆ 18.x) necess├írio para EF Core Design 10.0.2 -- AddStandardResilienceHandler simplifica configura├º├úo (3 chamadas ΓåÆ 1 com options) -- Upload timeout requer retry desabilitado (MaxRetryAttempts = 0) - -**Commits**: -- `b370b328`: "chore: update 39 nuget packages to latest stable versions" -- `949b6d3c`: "refactor: migrate from Polly.Extensions.Http to Microsoft.Extensions.Http.Resilience" +**Decisão de antecipação**: NX foi antecipado do pós-MVP para o Sprint 8B.2 porque o Provider App (Sprint 8C) e a migração Admin (Sprint 8D) se beneficiam diretamente do workspace unificado. Criar o NX antes desses projetos evita migração posterior mais custosa. --- - -### Γ£à Sprint 7.20 - Dashboard Charts & Data Mapping Fixes (5 Fev 2026) - -**Status**: CONCLU├ìDA (5 Fev 2026) -**Dura├º├úo**: 1 dia -**Branch**: `fix/aspire-initialization` (continua├º├úo) - -**Contexto**: Dashboard charts estavam exibindo mensagens de debug e o gr├ífico "Provedores por Tipo" estava vazio devido a incompatibilidade de mapeamento JSON entre backend e frontend. - -#### ≡ƒÄ» Objetivos - -1. Γ£à **Remover Mensagens de Debug** - Eliminar "Chart disabled for debugging" -2. Γ£à **Corrigir Gr├ífico Vazio** - Resolver problema de dados ausentes em "Provedores por Tipo" -3. Γ£à **Implementar Mapeamento JSON Correto** - Alinhar propriedades backend/frontend -4. Γ£à **Adicionar Helper Methods** - Criar m├⌐todos de formata├º├úo localizados - -#### ≡ƒöì Problema Identificado - -**Root Cause**: Property name mismatch entre backend e frontend - -- **Backend API** (`ProviderDto`): Retorna JSON com propriedade `type: 1` -- **Frontend DTO** (`ModuleProviderDto`): Esperava propriedade `ProviderType` -- **Resultado**: `ProviderType` ficava `null` no frontend, causando gr├ífico vazio - -**Investiga├º├úo**: -1. Γ£à Verificado `DevelopmentDataSeeder.cs` - Dados de seed CONT├èM tipos ("Individual", "Company") -2. Γ£à Analisado `GetProvidersEndpoint.cs` - Retorna `ProviderDto` com propriedade `Type` -3. Γ£à Inspecionado `ModuleProviderDto.cs` - Propriedade chamada `ProviderType` (mismatch!) -4. Γ£à Confirmado via `ProvidersEffects.cs` - Usa `IProvidersApi.GetProvidersAsync` - -#### ≡ƒ¢á∩╕Å Solu├º├╡es Implementadas - -**1. JSON Property Mapping** Γ£à: -```csharp -// src/Contracts/Contracts/Modules/Providers/DTOs/ModuleProviderDto.cs -using System.Text.Json.Serialization; - -public sealed record ModuleProviderDto( - Guid Id, - string Name, - string Email, - string Document, - [property: JsonPropertyName("type")] // ΓåÉ FIX: Mapeia "type" do JSON para "ProviderType" - string ProviderType, - string VerificationStatus, - DateTime CreatedAt, - DateTime UpdatedAt, - bool IsActive, - string? Phone = null); -``` - -**2. Debug Messages Removal** Γ£à: -```razor - - - - Chart disabled for debugging - @if (ProvidersState.Value.Providers.Count > 0) - - - - @if (ProvidersState.Value.Providers.Count > 0) -``` - -**3. Display Name Helper** Γ£à: -```csharp -// Dashboard.razor @code -private string GetProviderTypeDisplayName(ProviderType type) -{ - return type switch - { - ProviderType.Individual => "Pessoa F├¡sica", - ProviderType.Company => "Pessoa Jur├¡dica", - _ => type.ToString() - }; -} -``` - -**4. Chart Logic Simplification** Γ£à: -```csharp -// Removido c├│digo complexo de parsing int -// ANTES: int.TryParse(g.Key, out int typeValue) + ProviderTypeOrderInts lookup -// DEPOIS: Enum.TryParse(g.Key, true, out var typeEnum) + GetProviderTypeDisplayName() -``` - -#### ≡ƒôè Arquivos Modificados - -| Arquivo | Mudan├ºas | LOC | -|---------|----------|-----| -| `ModuleProviderDto.cs` | Adicionado `[JsonPropertyName("type")]` e using | +3 | -| `Dashboard.razor` | Removido debug text, adicionado helper method | +12, -15 | - -#### Γ£à Resultados Alcan├ºados - -- Γ£à **Gr├ífico "Provedores por Tipo"**: Agora exibe dados corretamente -- Γ£à **Mensagens de Debug**: Removidas de ambos os gr├íficos -- Γ£à **Build**: Sucesso sem erros (0 errors, 0 warnings) -- Γ£à **Mapeamento JSON**: Backend `type` ΓåÆ Frontend `ProviderType` funcionando -- Γ£à **Localiza├º├úo**: Labels em portugu├¬s ("Pessoa F├¡sica", "Pessoa Jur├¡dica") - -#### ≡ƒÄô Li├º├╡es Aprendidas - -1. **Property Naming Conventions**: Backend usa nomes curtos (`Type`), Frontend usa nomes descritivos (`ProviderType`) -2. **JSON Serialization**: `[JsonPropertyName]` ├⌐ essencial para alinhar DTOs entre camadas -3. **Record Positional Parameters**: Atributos requerem `[property: ...]` syntax -4. **Debug Messages**: Sempre remover antes de merge para evitar confus├úo em produ├º├úo - -#### ≡ƒö« Pr├│ximos Passos - -- [ ] Implementar "Atividades Recentes" (ver Fase 3+) -- [ ] Adicionar mais gr├íficos ao Dashboard (distribui├º├úo geogr├ífica, documentos pendentes) -- [ ] Criar testes bUnit para componentes de gr├íficos - -**Commits**: -- [hash]: "fix: add JsonPropertyName mapping for ProviderType in ModuleProviderDto" -- [hash]: "fix: remove debug messages and simplify chart logic in Dashboard" - ---- - -### Γ£à Sprint 7.16 - Technical Debt Sprint (17-21 Jan 2026) - -**Status**: Γ£à CONCLU├ìDA (17-21 Jan 2026) -**Dura├º├úo**: 1 semana (5 dias ├║teis) -**Objetivo**: Reduzir d├⌐bito t├⌐cnico ANTES de iniciar Customer App - -**Justificativa**: -- Customer App adicionar├í ~5000+ linhas de c├│digo novo -- Melhor resolver d├⌐bitos do Admin Portal ANTES de replicar patterns -- Keycloak automation ├⌐ BLOQUEADOR para Customer App (precisa de novo cliente OIDC) -- Quality improvements estabelecem padr├╡es para Customer App - ---- - -#### ≡ƒôï Tarefas Planejadas - -##### 1. ≡ƒöÉ Keycloak Client Automation (Dia 1-2, ~1 dia) - **BLOQUEADOR** - -**Prioridade**: CR├ìTICA - Customer App precisa de cliente OIDC "meajudaai-customer" - -**Entreg├íveis**: -- [ ] Script `infrastructure/keycloak/setup-keycloak-clients.ps1` - * Valida Keycloak rodando (HTTP health check) - * Obt├⌐m token admin via REST API - * Cria realm "MeAjudaAi" (se n├úo existir) - * Cria clientes "meajudaai-admin" e "meajudaai-customer" (OIDC, PKCE) - * Configura Redirect URIs (localhost + produ├º├úo) - * Cria roles "admin", "customer" - * Cria usu├írios demo (admin@meajudaai.com.br, customer@meajudaai.com.br) - * Exibe resumo de configura├º├úo -- [ ] Atualizar `docs/keycloak-admin-portal-setup.md` com se├º├úo "Automated Setup" -- [ ] Integrar script em `scripts/dev.ps1` (opcional - chamar setup-keycloak-clients.ps1) - -**API Keycloak Admin REST**: -- Endpoint: `POST /auth/admin/realms/{realm}/clients` -- Autentica├º├úo: Bearer token - -**Benef├¡cios**: -- Γ£à Customer App pronto para desenvolvimento (cliente configurado) -- Γ£à Onboarding em 1 comando: `.\setup-keycloak-clients.ps1` -- Γ£à Elimina 15 passos manuais documentados - ---- - -##### 2. ≡ƒÄ¿ Frontend Analyzer Warnings (Dia 2-3, ~1 dia) - -**Prioridade**: ALTA - Code quality antes de expandir codebase - -**Warnings a Resolver**: - -**S2094 - Empty Records (6 ocorr├¬ncias)**: -```csharp -// ANTES -public sealed record LoadProvidersAction { } - -// DEPOIS - Op├º├úo 1: Adicionar propriedade ├║til -public sealed record LoadProvidersAction -{ - public bool ForceRefresh { get; init; } -} - -// DEPOIS - Op├º├úo 2: Justificar supress├úo -#pragma warning disable S2094 // Empty action by design (Redux pattern) -public sealed record LoadProvidersAction { } -#pragma warning restore S2094 -``` - -**S2953 - Dispose Pattern (1 ocorr├¬ncia)**: -```csharp -// ANTES: App.razor -public void Dispose() { ... } - -// DEPOIS -public class App : IDisposable -{ - public void Dispose() { ... } -} -``` - -**S2933 - Readonly Fields (1 ocorr├¬ncia)**: -```csharp -// ANTES -private MudTheme _theme = new(); - -// DEPOIS -private readonly MudTheme _theme = new(); -``` - -**MUD0002 - Casing (3 ocorr├¬ncias)**: -```razor - - - - - -``` - -**Entreg├íveis**: -- [ ] Resolver todos os 11 warnings (ou justificar supress├╡es) -- [ ] Remover regras do `.editorconfig` ap├│s corre├º├úo -- [ ] Build com **0 warnings** - ---- - -##### 3. ≡ƒôè Frontend Test Coverage (Dia 3-5, ~1-2 dias) - -**Prioridade**: ALTA - Confian├ºa em Admin Portal antes de Customer App - -**Meta**: 10 ΓåÆ 30-40 testes bUnit - -**Testes Novos (20-30 testes)**: - -**Fluxor State Management (8 testes)**: -- `ProvidersReducers`: LoadSuccess, LoadFailure, SetFilters, SetSorting -- `DocumentsReducers`: UploadSuccess, VerificationUpdate -- `ServiceCatalogsReducers`: CreateSuccess, UpdateSuccess - -**Components (12 testes)**: -- `Providers.razor`: rendering, search, pagination (3 testes) -- `Documents.razor`: upload workflow, verification (3 testes) -- `CreateProviderDialog`: form validation, submit (2 testes) -- `EditProviderDialog`: data binding, update (2 testes) -- `LanguageSwitcher`: culture change, persistence (2 testes) - -**Services (5 testes)**: -- `LocalizationService`: SetCulture, GetString, fallback -- `ErrorHandlingService`: retry logic, status mapping - -**Effects (3 testes)**: -- Mock `IProvidersApi.GetPagedProvidersAsync` -- Verificar dispatches Success/Failure -- Testar error handling - -**Infraestrutura**: -- Criar `TestContext` base reutiliz├ível -- Configurar `JSRuntimeMode.Loose` -- Registrar `MudServices` e `Fluxor` - -**Entreg├íveis**: -- [ ] 30-40 testes bUnit (3x aumento) -- [ ] Cobertura ~40-50% de componentes cr├¡ticos -- [ ] CI/CD passing (master-ci-cd.yml) - ---- - -##### 4. ≡ƒô¥ Records Standardization (Dia 5, ~0.5 dia) - -**Prioridade**: M├ëDIA - Padroniza├º├úo importante - -**Objetivo**: Padronizar uso de `record class` vs `record` vs `class` no projeto. - -**Auditoria**: -```powershell -# Buscar todos os records no projeto -Get-ChildItem -Recurse -Include *.cs | Select-String "record " -``` - -**Padr├╡es a Estabelecer**: -- DTOs: `public record Dto` (imut├ível) -- Requests: `public sealed record Request` (imut├ível) -- Responses: `public sealed record Response` (imut├ível) -- Fluxor Actions: `public sealed record Action` (imut├ível) -- Fluxor State: `public sealed record State` (imut├ível) -- Entities: `public class ` (mut├ível, EF Core) - -**Entreg├íveis**: -- [ ] Documentar padr├úo em `docs/architecture.md` se├º├úo "C# Coding Standards" -- [ ] Converter records inconsistentes (se necess├írio) -- [ ] Adicionar analyzer rule para enforcement futuro - ---- - -##### 5. ≡ƒº¬ SearchProviders E2E Tests Γܬ MOVIDO PARA SPRINT 9 - -**Prioridade**: M├ëDIA - MOVIDO PARA SPRINT 9 (Buffer) - -**Objetivo**: Testar busca geolocalizada end-to-end. - -**Status**: Γܬ MOVIDO PARA SPRINT 9 - Task opcional, n├úo cr├¡tica para Customer App - -**Justificativa da Movimenta├º├úo**: -- Sprint 7.16 completou 4/4 tarefas obrigat├│rias (Keycloak, Warnings, Tests, Records) -- E2E tests marcados como OPCIONAL desde o planejamento -- N├úo bloqueiam Sprint 8 (Customer App) -- Melhor executar com calma no Sprint 9 (Buffer) sem press├úo de deadline - -**Entreg├íveis** (ser├úo executados no Sprint 9): -- [ ] Teste: Buscar providers por servi├ºo + raio (2km, 5km, 10km) -- [ ] Teste: Validar ordena├º├úo por dist├óncia -- [ ] Teste: Validar restri├º├úo geogr├ífica (AllowedCities) -- [ ] Teste: Performance (<500ms para 1000 providers) - -**Estimativa**: 1-2 dias (Sprint 9) - ---- - -#### ≡ƒôè Resultado Esperado Sprint 7.16 - -**D├⌐bito T├⌐cnico Reduzido**: -- Γ£à Keycloak automation completo (bloqueador removido) -- Γ£à 0 warnings no Admin Portal (S2094, S2953, S2933, MUD0002) -- Γ£à 30-40 testes bUnit (confian├ºa 3x maior) -- Γ£à Records padronizados (consist├¬ncia) -- Γܬ SearchProviders E2E (MOVIDO para Sprint 9 - n├úo cr├¡tico) - -**Quality Metrics**: -- **Build**: 0 errors, 0 warnings -- **Tests**: 1245 backend + 43 frontend bUnit = **1288 testes** -- **Coverage**: Backend 90.56% (frontend bUnit sem m├⌐trica - foco em quantidade de testes) -- **Technical Debt**: Reduzido de 313 linhas ΓåÆ ~150 linhas - -**Pronto para Customer App**: -- Γ£à Keycloak configurado (cliente meajudaai-customer) -- Γ£à Admin Portal com qualidade m├íxima (patterns estabelecidos) -- Γ£à Test infrastructure robusta (replic├ível no Customer App) -- Γ£à Zero distra├º├╡es (d├⌐bito t├⌐cnico minimizado) - -**Commits Estimados**: -- `feat(sprint-7.16): add Keycloak client automation script` -- `fix(sprint-7.16): resolve all frontend analyzer warnings` -- `test(sprint-7.16): increase bUnit coverage to 30-40 tests` -- `refactor(sprint-7.16): standardize record usage across project` - ---- - -### Γ£à Sprint 8A - Customer Web App (Conclu├¡da) - -**Status**: CONCLU├ìDA (5-13 Fev 2026) -**Foco**: Refinamento de Layout e UX (Home & Search) - -**Atividades Realizadas**: -1. **Home Page Layout Refinement** Γ£à - - Restaurada se├º├úo "Como funciona?" (How It Works) ap├│s "Conhe├ºa o MeAjudaA├¡". - - Ajustado posicionamento para melhorar fluxo de conte├║do (Promessa -> Confian├ºa -> Processo). - - Corrigidos warnings de imagens (aspect ratio, sizes). - - Ajustados espa├ºamentos e alinhamentos (Hero, City Search vertical center). - -2. **Search Page Layout & UX** Γ£à - - Removido limite de largura (`max-w-6xl`) para aproveitar tela cheia. - - Service Tags movidas para largura total, centralizadas em desktop. - - Mock de Service Tags atualizado para "Top 10 Servi├ºos Populares" (Pedreiro, Eletricista, etc.). - - Melhorada experi├¬ncia em mobile com scroll horizontal. - -**Pr├│ximos Passos (Imediato)**: -- Integrar Service Tags com backend real (popularidade/regional). -- Implementar filtros avan├ºados. - ---- - -### Γ£à Sprint 8B - Authentication & Onboarding Flow - CONCLU├ìDO - -**Periodo Estimado**: 19 Fev - 4 Mar 2026 -**Foco**: Fluxos de Cadastro e Login Segmentados (Cliente vs Prestador) - -**Regras de Neg├│cio e UX**: - -**1. Ponto de Entrada Unificado** -- Bot├úo "Cadastre-se Gr├ítis" na Home/Header. -- **Modal de Sele├º├úo** (Inspirado em refer├¬ncia visual): - - Op├º├úo A: "Quero ser cliente" (Encontrar melhores acompanhantes/prestadores). - - Op├º├úo B: "Sou prestador" (Divulgar servi├ºos). - -**2. Fluxo do Cliente (Customer Flow)** -- **Login/Cadastro**: - - Social Login: Google, Facebook, Instagram. - - Manual: Email + Senha. -- **Dados**: - - Validar necessidade de endere├ºo (Possivelmente opcional no cadastro, obrigat├│rio no agendamento). - -**3. Fluxo do Prestador (Provider Flow)** -- **Redirecionamento**: Ao clicar em "Sou prestador", redirecionar para landing page espec├¡fica de prestadores (modelo visual refer├¬ncia #3). -- **Etapa 1: Cadastro B├ísico**: - - Social Login ou Manual. - - Dados B├ísicos: Nome, Telefone/WhatsApp (validado via OTP se poss├¡vel). -- **Etapa 2: Verifica├º├úo de Seguran├ºa (Obrigat├│ria)**: - - Upload de Documentos (RG/CNH). - - Valida├º├úo de Antecedentes Criminais. - - Biometria Facial (Liveness Check) para evitar fraudes. -- **Conformidade LGPD & Seguran├ºa**: - - **Consentimento Expl├¡cito**: Coleta de aceite inequ├¡voco para tratamento de dados sens├¡veis (biometria, antecedentes), detalhando finalidade e base legal (Preven├º├úo ├á Fraude/Leg├¡timo Interesse). - - **Pol├¡tica de Reten├º├úo**: Defini├º├úo clara de prazos de armazenamento e fluxo de exclus├úo autom├ítica ap├│s inatividade ou solicita├º├úo. - - **Operadores de Dados**: Contratos com vendors (ex: servi├ºo de biometria) exigindo compliance LGPD/GDPR e Acordos de Processamento de Dados (DPA). - - **Direitos do Titular**: Fluxos automatizados para solicita├º├úo de exporta├º├úo (portabilidade) e anonimiza├º├úo/exclus├úo de dados. - - **DPIA**: Realiza├º├úo de Relat├│rio de Impacto ├á Prote├º├úo de Dados (RIPD) espec├¡fico para o tratamento de dados biom├⌐tricos. - - **Seguran├ºa**: Criptografia em repouso (AES-256) e em tr├ónsito (TLS 1.3). Divulga├º├úo transparente do uso de reCAPTCHA v3 e seus termos. -- **Prote├º├úo**: Integra├º├úo com Google reCAPTCHA v3 em todo o fluxo. - -**Entreg├íveis**: -- [ ] Componente `AuthModal` com sele├º├úo de perfil. -- [ ] Integra├º├úo `NextAuth.js` com Providers (Google, FB, Instagram) e Credentials. -- [ ] P├ígina de Onboarding de Prestadores (Step-by-step wizard). -- [ ] Integra├º├úo com servi├ºo de verifica├º├úo de documentos/biometria. - ---- - -### Γ£à Sprint 8C - Provider Web App (React + NX) - CONCLUÍDO (21 Mar 2026) - -**Periodo Real**: 19 Mar - 21 Mar 2026 -**Foco**: App de Administra├º├úo de Perfil para Prestadores -**Status**: Γ£à CONCLUÍDO - -**Entregáveis Implementados**: -- ✅ **Nx Integration**: `MeAjudaAi.Web.Provider` integrado ao workspace Nx -- ✅ **Onboarding/basic-info**: Conectado à API (`apiMeGet`/`apiMePut`) com validação Zod -- ✅ **Onboarding/documents**: Upload via SAS URL para Azure Blob Storage -- ✅ **Dashboard Real Data**: Página principal (`/`) substituída por dados reais via `apiMeGet` -- ✅ **Provider Public Profile**: Nova rota `/provider/[slug]` para perfis públicos com slugs SEO-friendly -- ✅ **Provider Profile Management**: - - `/alterar-dados` - Edição completa via `apiMePut` - - `/configuracoes` - Toggle de visibilidade + delete account com confirmação LGPD -- ✅ **Slug URLs**: Perfis públicos acessíveis via slugs (ex: `/provider/joao-silva-a1b2c3d4`) - ---- - -### ΓÅ│ Sprint 8D - Admin Portal Migration (Blazor ΓåÆ React + NX) - -**Periodo Estimado**: 2 - 15 Abr 2026 -**Foco**: Migra├º├úo do Admin Portal de Blazor WASM para React dentro do workspace NX -**Branch**: (a ser criada: `feature/sprint-8d-admin-migration`) - -**Contexto**: Terceiro app React no workspace NX. Reutiliza padr├╡es e shared libs consolidados pelo Customer (Sprint 8A) e Provider App (Sprint 8C). Elimina dual-stack (Blazor + React) em favor de single-stack React. - -**Escopo**: -- Criar `apps/admin-web` dentro do workspace NX (Next.js + Tailwind v4). -- Migrar todas as funcionalidades existentes do Blazor Admin Portal: - - Dashboard com KPIs e gr├íficos (Providers por status/tipo) - - CRUD Providers (Create, Update, Delete, Verify) - - Gest├úo de Documentos (Upload, Verifica├º├úo, Rejei├º├úo) - - Gest├úo de Service Catalogs (Categorias + Servi├ºos) - - Gest├úo de Restri├º├╡es Geogr├íficas (AllowedCities) - - Dark Mode, Localiza├º├úo (i18n), Acessibilidade -- Substituir Fluxor por Zustand ou Redux Toolkit (state management React). -- Substituir Refit/C# DTOs por `libs/api-client` (gerado via OpenAPI ou manual). -- Manter autentica├º├úo Keycloak (cliente `meajudaai-admin`). -- Estilo visual unificado com Customer e Provider Apps. -- Remover projeto Blazor WASM ap├│s migra├º├úo completa e valida├º├úo. - ---- - -### ΓÅ│ Sprint 8E - Mobile App (React Native + Expo) - -**Periodo Estimado**: 16 - 29 Abr 2026 -**Foco**: App Mobile Nativo (iOS/Android) com Expo -**Branch**: (a ser criada: `feature/sprint-8e-mobile-app`) - -**Escopo**: -- Criar `apps/mobile` dentro do workspace NX (React Native + Expo). -- Portar funcionalidades do Customer Web App para Mobile. -- Reutilizar l├│gica de neg├│cio e autentica├º├úo via shared libs NX. -- Notifica├º├╡es Push. - ---- - -**Status**: SKIPPED durante Parts 10-15 (escopo muito grande) -**Prioridade**: Alta (recomendado antes do MVP) -**Estimativa**: 3-5 dias de sprint dedicado - -**Contexto**: A Part 13 foi intencionalmente pulada durante a implementa├º├úo das Parts 10-15 (melhorias menores) por ser muito extensa e merecer um sprint dedicado. Testes unit├írios frontend s├úo cr├¡ticos para manutenibilidade e confian├ºa no c├│digo, mas requerem setup completo de infraestrutura de testes. - -**Escopo Planejado**: - -**1. Infraestrutura de Testes** (1 dia): -- Criar projeto `MeAjudaAi.Web.Admin.Tests` -- Adicionar pacotes: bUnit, Moq, FluentAssertions, xUnit -- Configurar test host e service mocks -- Setup de TestContext base reutiliz├ível - -**2. Testes de Fluxor State Management** (1-2 dias): -- **Reducers**: 15+ testes para state mutations - * ProvidersReducers: LoadSuccess, LoadFailure, SetFilters, SetSorting - * DocumentsReducers: UploadSuccess, VerificationUpdate - * ServiceCatalogsReducers: CRUD operations - * LocationsReducers: LoadCities, FilterByState - * ErrorReducers: SetGlobalError, ClearError, RetryAfterError -- **Actions**: Verificar payloads corretos -- **Features**: Initial state validation - -**3. Testes de Effects** (1 dia): -- Mock de IProvidersApi, IDocumentsApi, IServiceCatalogsApi -- Test de retry logic em ErrorHandlingService -- Verificar dispatches corretos (Success/Failure actions) -- Test de autoriza├º├úo e permiss├╡es - -**4. Testes de Componentes** (1-2 dias): -- **Pages**: - * Providers.razor: rendering, search, pagination - * Documents.razor: upload, verification workflow - * ServiceCatalogs.razor: category/service CRUD - * Dashboard.razor: charts rendering -- **Dialogs**: - * CreateProviderDialog: form validation - * EditProviderDialog: data binding - * UploadDocumentDialog: file upload mock - * VerifyProviderDialog: status change -- **Shared Components**: - * LanguageSwitcher: culture change - * LiveRegionAnnouncer: accessibility - * ErrorBoundaryContent: error recovery - -**5. Testes de Servi├ºos** (0.5 dia): -- LocalizationService: culture switching, string retrieval -- ErrorHandlingService: retry logic, status code mapping -- LiveRegionService: announcement queue -- ErrorLoggingService: correlation IDs -- PermissionService: policy checks - -**Meta de Cobertura**: -- **Reducers**: >95% (l├│gica pura, f├ícil de testar) -- **Effects**: >80% (com mocks de APIs) -- **Components**: >70% (rendering e intera├º├╡es b├ísicas) -- **Services**: >90% (l├│gica de neg├│cio) -- **Geral**: >80% code coverage - -**Benef├¡cios Esperados**: -- Γ£à Confidence em refactorings futuros -- Γ£à Documenta├º├úo viva do comportamento esperado -- Γ£à Detec├º├úo precoce de regress├╡es -- Γ£à Facilita onboarding de novos devs -- Γ£à Reduz bugs em produ├º├úo - -**Ferramentas e Patterns**: -```csharp -// Exemplo de teste de Reducer -[Fact] -public void LoadProvidersSuccessAction_Should_UpdateState() -{ - // Arrange - var initialState = new ProvidersState(isLoading: true, providers: []); - var providers = new List { /* mock data */ }; - var action = new LoadProvidersSuccessAction(providers, totalItems: 10, pageNumber: 1, pageSize: 10); - - // Act - var newState = ProvidersReducers.OnLoadProvidersSuccess(initialState, action); - - // Assert - newState.IsLoading.Should().BeFalse(); - newState.Providers.Should().HaveCount(1); - newState.TotalItems.Should().Be(10); -} - -// Exemplo de teste de Component -[Fact] -public void LanguageSwitcher_Should_ChangeCulture() -{ - // Arrange - using var ctx = new TestContext(); - ctx.Services.AddScoped(); - var component = ctx.RenderComponent(); - - // Act - var enButton = component.Find("button[data-lang='en-US']"); - enButton.Click(); - - // Assert - var localization = ctx.Services.GetRequiredService(); - localization.CurrentCulture.Name.Should().Be("en-US"); -} -``` - -**Prioriza├º├úo Sugerida**: -1. **Cr├¡tico (antes do MVP)**: Reducers + Effects + ErrorHandlingService -2. **Importante (pr├⌐-MVP)**: Componentes principais (Providers, Documents) -3. **Nice-to-have (p├│s-MVP)**: Componentes de UI (dialogs, shared) - -**Recomenda├º├úo**: Implementar em **Sprint 8.5** (entre Customer App e Buffer) ou dedicar 1 semana do Sprint 9 (Buffer) para esta tarefa. Frontend tests s├úo investimento de longo prazo essencial para manutenibilidade. - ---- - -### Γ£à Sprint 7 - Blazor Admin Portal Features - CONCLU├ìDA (6-7 Jan 2026) - -**Branch**: `blazor-admin-portal-features` (MERGED to master) - -**Objetivos**: -1. Γ£à **CRUD Completo de Providers** (6-7 Jan 2026) - Create, Update, Delete, Verify -2. Γ£à **Gest├úo de Documentos** (7 Jan 2026) - Upload, verifica├º├úo, deletion workflow -3. Γ£à **Gest├úo de Service Catalogs** (7 Jan 2026) - CRUD de categorias e servi├ºos -4. Γ£à **Gest├úo de Restri├º├╡es Geogr├íficas** (7 Jan 2026) - UI para AllowedCities com banco de dados -5. Γ£à **Gr├íficos Dashboard** (7 Jan 2026) - MudCharts com providers por status e evolu├º├úo temporal -6. Γ£à **Testes** (7 Jan 2026) - Aumentar cobertura para 30 testes bUnit - -**Progresso Atual**: 6/6 features completas Γ£à **SPRINT 7 CONCLU├ìDO 100%!** - -**Detalhamento - Provider CRUD** Γ£à: -- IProvidersApi enhanced: CreateProviderAsync, UpdateProviderAsync, DeleteProviderAsync, UpdateVerificationStatusAsync -- CreateProviderDialog: formul├írio completo com valida├º├úo (ProviderType, Name, FantasyName, Document, Email, Phone, Description, Address) -- EditProviderDialog: edi├º├úo simplificada (nome/telefone - aguardando DTO enriquecido do backend) -- VerifyProviderDialog: mudan├ºa de status de verifica├º├úo (Verified, Rejected, Pending + optional notes) -- Providers.razor: action buttons (Edit, Delete, Verify) com MessageBox confirmation -- Result error handling pattern em todas opera├º├╡es -- Portuguese labels + Snackbar notifications -- Build sucesso (19 warnings Sonar apenas) -- Commit: cd2be7f6 "feat(admin): complete Provider CRUD operations" - -**Detalhamento - Documents Management** Γ£à: -- DocumentsState/Actions/Reducers/Effects: Fluxor pattern completo -- Documents.razor: p├ígina com provider selector e listagem de documentos -- MudDataGrid com status chips coloridos (Verified=Success, Rejected=Error, Pending=Warning, Uploaded=Info) -- ProviderSelectorDialog: sele├º├úo de provider da lista existente -- UploadDocumentDialog: MudFileUpload com tipos de documento (RG, CNH, CPF, CNPJ, Comprovante, Outros) -- RequestVerification action via IDocumentsApi.RequestDocumentVerificationAsync -- DeleteDocument com confirma├º├úo MessageBox -- Real-time status updates via Fluxor Dispatch -- Portuguese labels + Snackbar notifications -- Build sucesso (28 warnings Sonar apenas) -- Commit: e033488d "feat(admin): implement Documents management feature" - -**Detalhamento - Service Catalogs** Γ£à: -- IServiceCatalogsApi enhanced: 10 m├⌐todos (Create, Update, Delete, Activate, Deactivate para Categories e Services) -- ServiceCatalogsState/Actions/Reducers/Effects: Fluxor pattern completo -- Categories.razor: full CRUD page com MudDataGrid, status chips, action buttons -- Services.razor: full CRUD page com category relationship e MudDataGrid -- CreateCategoryDialog, EditCategoryDialog: forms com Name, Description, DisplayOrder -- CreateServiceDialog, EditServiceDialog: forms com CategoryId (dropdown), Name, Description, DisplayOrder -- Activate/Deactivate toggles para ambos -- Delete confirmations com MessageBox -- Portuguese labels + Snackbar notifications -- Build sucesso (37 warnings Sonar/MudBlazor apenas) -- Commit: bd0c46b3 "feat(admin): implement Service Catalogs CRUD (Categories + Services)" - -**Detalhamento - Geographic Restrictions** Γ£à: -- ILocationsApi j├í possu├¡a CRUD completo (Create, Update, Delete, GetAll, GetById, GetByState) -- LocationsState/Actions/Reducers/Effects: Fluxor pattern completo -- AllowedCities.razor: full CRUD page com MudDataGrid -- CreateAllowedCityDialog: formul├írio com City, State, Country, Latitude, Longitude, ServiceRadiusKm, IsActive -- EditAllowedCityDialog: mesmo formul├írio para edi├º├úo -- MudDataGrid com coordenadas em formato F6 (6 decimais), status chips (Ativa/Inativa) -- Toggle activation via MudSwitch (updates backend via UpdateAllowedCityAsync) -- Delete confirmation com MessageBox -- Portuguese labels + Snackbar notifications -- Build sucesso (42 warnings Sonar/MudBlazor apenas) -- Commit: 3317ace3 "feat(admin): implement Geographic Restrictions - AllowedCities UI" - -**Detalhamento - Dashboard Charts** Γ£à: -- Dashboard.razor enhanced com 2 charts interativos (MudBlazor built-in charts) -- Provider Status Donut Chart: agrupa providers por VerificationStatus (Verified, Pending, Rejected) -- Provider Type Pie Chart: distribui├º├úo entre Individual (Pessoa F├¡sica) e Company (Empresa) -- Usa ProvidersState existente (sem novos endpoints de backend) -- OnAfterRender lifecycle hook para update de dados quando providers carregam -- UpdateChartData() m├⌐todo com GroupBy LINQ queries -- Portuguese labels para tipos de provider -- Empty state messages quando n├úo h├í providers cadastrados -- MudChart components com Width="300px", Height="300px", LegendPosition.Bottom -- Build sucesso (43 warnings Sonar/MudBlazor apenas) -- Commit: 0e0d0d81 "feat(admin): implement Dashboard Charts with MudBlazor" - -**Detalhamento - Testes bUnit** Γ£à: -- 30 testes bUnit criados (objetivo: 30+) - era 10, adicionados 20 novos -- CreateProviderDialogTests: 4 testes (form fields, submit button, provider type, MudForm) -- DocumentsPageTests: 5 testes (provider selector, upload button, loading, document list, error) -- CategoriesPageTests: 4 testes (load action, create button, list, loading) -- ServicesPageTests: 3 testes (load actions, create button, list) -- AllowedCitiesPageTests: 4 testes (load action, create button, list, loading) -- Todos seguem pattern: Mock IState/IDispatcher/IApi, AddMudServices, JSRuntimeMode.Loose -- Verificam rendering, state management, user interactions -- Namespaces corrigidos: Modules.*.DTOs -- Build sucesso (sem erros de compila├º├úo) -- Commit: 2a082840 "test(admin): increase bUnit test coverage to 30 tests" - ---- - -### Γ£à Sprint 6 - Blazor Admin Portal Setup - CONCLU├ìDA (30 Dez 2025 - 5 Jan 2026) - -**Status**: MERGED to master (5 Jan 2026) - -**Principais Conquistas**: -1. **Projeto Blazor WASM Configurado** Γ£à - - .NET 10 com target `net10.0-browser` - - MudBlazor 7.21.0 (Material Design UI library) - - Fluxor 6.1.0 (Redux-pattern state management) - - Refit 9.0.2 (Type-safe HTTP clients) - - Bug workaround: `CompressionEnabled=false` (static assets .NET 10) - -2. **Autentica├º├úo Keycloak OIDC Completa** Γ£à - - Microsoft.AspNetCore.Components.WebAssembly.Authentication - - Login/Logout flows implementados - - Authentication.razor com 6 estados (LoggingIn, CompletingLoggingIn, etc.) - - BaseAddressAuthorizationMessageHandler configurado - - **Token Storage**: SessionStorage (Blazor WASM padr├úo) - - **Refresh Strategy**: Autom├ítico via OIDC library (silent refresh) - - **SDKs Refit**: Interfaces manuais com atributos Refit (n├úo code-generated) - - Documenta├º├úo completa em `docs/keycloak-admin-portal-setup.md` - -3. **Providers Feature (READ-ONLY)** Γ£à - - Fluxor store completo (State/Actions/Reducers/Effects) - - MudDataGrid com pagina├º├úo server-side - - IProvidersApi via Refit com autentica├º├úo - - PagedResult correto (Client.Contracts.Api) - - VERIFIED_STATUS constant (type-safe) - - Portuguese error messages - -4. **Dashboard com KPIs** Γ£à - - 3 KPIs: Total Providers, Pending Verifications, Active Services - - IServiceCatalogsApi integrado (contagem real de servi├ºos) - - MudCards com Material icons - - Fluxor stores para Dashboard state - - Loading states e error handling - -5. **Dark Mode com Fluxor** Γ£à - - ThemeState management (IsDarkMode boolean) - - Toggle button em MainLayout - - MudThemeProvider two-way binding - -6. **Layout Base** Γ£à - - MainLayout.razor com MudDrawer + MudAppBar - - NavMenu.razor com navega├º├úo - - User menu com AuthorizeView - - Responsive design (Material Design) - -7. **Testes bUnit + xUnit** Γ£à - - 10 testes criados (ProvidersPageTests, DashboardPageTests, DarkModeToggleTests) - - JSInterop mock configurado (JSRuntimeMode.Loose) - - MudServices registrados em TestContext - - CI/CD integration (master-ci-cd.yml + pr-validation.yml) - -8. **Localiza├º├úo Portuguesa** Γ£à - - Todos coment├írios inline em portugu├¬s - - Mensagens de erro em portugu├¬s - - UI messages traduzidas (Authentication.razor) - - Projeto language policy compliance - -9. **Integra├º├úo Aspire** Γ£à - - Admin portal registrado em AppHost - - Environment variables configuradas (ApiBaseUrl, Keycloak) - - Build e execu├º├úo via `dotnet run --project src/Aspire/MeAjudaAi.AppHost` - -10. **Documenta├º├úo** Γ£à - - docs/keycloak-admin-portal-setup.md (manual configura├º├úo) - - docs/testing/bunit-ci-cd-practices.md (atualizado) - - Roadmap atualizado com progresso Sprint 6 - -11. **SDKs Completos para Sprint 7** Γ£à (6 Jan 2026) - - IDocumentsApi: Upload, verifica├º├úo, gest├úo de documentos de providers - - ILocationsApi: CRUD de cidades permitidas (AllowedCities) - - DTOs criados: ModuleAllowedCityDto, Create/UpdateAllowedCityRequestDto - - README melhorado: conceito de SDK, diagrama arquitetural, compara├º├úo manual vs SDK - - 4/4 SDKs necess├írios para Admin Portal (Providers, Documents, ServiceCatalogs, Locations) - -**Resultado Alcan├ºado**: -- Γ£à Blazor Admin Portal 100% funcional via Aspire -- Γ£à Login/Logout Keycloak funcionando -- Γ£à Providers listagem paginada (read-only) -- Γ£à Dashboard com 3 KPIs reais (IServiceCatalogsApi) -- Γ£à Dark mode toggle -- Γ£à 10 testes bUnit (build verde) -- Γ£à Portuguese localization completa -- Γ£à 0 erros build (10 warnings - analyzers apenas) -- Γ£à **4 SDKs completos** para Admin Portal (IProvidersApi, IDocumentsApi, IServiceCatalogsApi, ILocationsApi) -- Γ£à **Documenta├º├úo SDK** melhorada (conceito, arquitetura, exemplos pr├íticos) - -**Γ£à Pr├│xima Etapa Conclu├¡da: Sprint 7 - Blazor Admin Portal Features** (6-7 Jan 2026) -- Γ£à CRUD completo de Providers (create, update, delete, verify) -- Γ£à Gest├úo de Documentos (upload, verifica├º├úo, rejection) -- Γ£à Gest├úo de Service Catalogs (categorias + servi├ºos) -- Γ£à Gest├úo de Restri├º├╡es Geogr├íficas (UI para AllowedCities) -- Γ£à Gr├íficos Dashboard (MudCharts - providers por status, evolu├º├úo temporal) -- Γ£à Aumentar cobertura de testes (30+ testes bUnit) - ---- - -## Γ£à Sprint 5.5 - Refactor & Cleanup (19-30 Dez 2025) - -**Status**: CONCLU├ìDA - -**Principais Conquistas**: -1. **Refatora├º├úo MeAjudaAi.Shared.Messaging** Γ£à - - Factories organizados em pasta dedicada (`Messaging/Factories/`) - - Services organizados em pasta dedicada (`Messaging/Services/`) - - Options organizados em pasta dedicada (`Messaging/Options/`) - - 4 arquivos: ServiceBusOptions, MessageBusOptions, RabbitMqOptions, DeadLetterOptions - - IMessageBusFactory + MessageBusFactory separados - - IDeadLetterServiceFactory + DeadLetterServiceFactory separados - - 1245/1245 testes passando - -2. **Extensions Padronizadas** Γ£à - - 14 arquivos consolidados: CachingExtensions, CommandsExtensions, DatabaseExtensions, etc. - - BusinessMetricsMiddlewareExtensions extra├¡do para arquivo pr├│prio - - Monitoring folder consolidation completo - - Removidos 13 arquivos obsoletos (Extensions.cs gen├⌐ricos + subpastas) - -3. **Extension Members (C# 14)** Γ£à - - EnumExtensions migrado para nova sintaxe `extension(string value)` - - 18/18 testes passando (100% compatibilidade) - - Documentado em architecture.md - se├º├úo "C# 14 Features Utilizados" - - Avaliado DocumentExtensions (n├úo adequado para extension properties) - -4. **TODOs Resolvidos** Γ£à - - 12/12 TODOs no c├│digo resolvidos ou documentados - - Remaining issues movidos para technical-debt.md com prioriza├º├úo - - api-reference.md removido (redundante com ReDoc + api-spec.json) - -5. **Documenta├º├úo Atualizada** Γ£à - - architecture.md atualizado com C# 14 features - - technical-debt.md atualizado com status atual - - roadmap.md atualizado com Sprint 5.5 completion - - 0 warnings in build - -**Γ£à Fase 1.5: CONCLU├ìDA** (21 Nov - 10 Dez 2025) -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 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 - -**Γ£à Sprint 4: CONCLU├ìDO** (14 Dez - 16 Dez 2025) -Health Checks Robustos + Data Seeding para MVP - TODAS AS PARTES FINALIZADAS: -- Γ£à Health Checks: DatabasePerformanceHealthCheck (lat├¬ncia <100ms healthy, <500ms degraded) -- Γ£à Health Checks: ExternalServicesHealthCheck (Keycloak + IBGE API + Redis) -- Γ£à Health Checks: HelpProcessingHealthCheck (sistema de ajuda operacional) -- Γ£à Health Endpoints: /health, /health/live, /health/ready com JSON responses -- Γ£à Health Dashboard: Dashboard nativo do Aspire (decis├úo arquitetural - n├úo usar AspNetCore.HealthChecks.UI) -- Γ£à Health Packages: AspNetCore.HealthChecks.Npgsql 9.0.0, .Redis 8.0.1 -- Γ£à Redis Health Check: Configurado via AddRedis() com tags 'ready', 'cache' -- Γ£à Data Seeding: infrastructure/database/seeds/01-seed-service-catalogs.sql (8 categorias + 12 servi├ºos) -- Γ£à Seed Automation: Docker Compose executa seeds automaticamente na inicializa├º├úo -- Γ£à Project Structure: Reorganiza├º├úo - automation/ ΓåÆ infrastructure/automation/, seeds em infrastructure/database/seeds/ -- Γ£à Documentation: README.md, scripts/README.md, infrastructure/database/README.md + docs/future-external-services.md -- Γ£à MetricsCollectorService: Implementado com IServiceScopeFactory (4 TODOs resolvidos) -- Γ£à Unit Tests: 14 testes para ExternalServicesHealthCheck (6 novos para IBGE API) -- Γ£à Integration Tests: 9 testes para DataSeeding (categorias, servi├ºos, idempot├¬ncia) -- Γ£à Future Services Documentation: Documentado OCR, payments, SMS/email (quando implementar) -- Γ£à Code Review: Logs traduzidos para ingl├¬s conforme pol├¡tica (Program.cs - 3 mensagens) -- Γ£à Markdown Linting: technical-debt.md corrigido (code blocks, URLs, headings) -- Γ£à Architecture Test: PermissionHealthCheckExtensions exception documentada (namespace vs folder structure) - -**Γ£à Sprint 5: CONCLU├ìDO ANTECIPADAMENTE** (Tarefas completadas nos Sprints 3-4) -Todas as tarefas planejadas j├í foram implementadas: -- Γ£à NSubstitute ΓåÆ Moq migration (Sprint 3) -- Γ£à UuidGenerator unification (commit 0a448106) -- Γ£à .slnx migration (commit 1de5dc1a) -- Γ£à Design patterns documentation (architecture.md) -- Γ£à Bruno collections para todos m├│dulos (Users, Providers, Documents) - -**ΓÅ│ Sprint 5.5: CONCLU├ìDA** (19-20 Dez 2025) Γ£à -**Branch**: `feature/refactor-and-cleanup` -**Objetivo**: Refatora├º├úo t├⌐cnica e redu├º├úo de d├⌐bito t├⌐cnico antes do frontend - -**Γ£à Refatoramento de Testes Completado** (20 Dez 2025): -- Γ£à Reorganiza├º├úo estrutural de MeAjudaAi.Shared.Tests (TestInfrastructure com 8 subpastas) -- Γ£à ModuleExtensionsTests movidos para m├│dulos individuais (Documents, Providers, ServiceCatalogs, Users) -- Γ£à Tradu├º├úo de ~35 coment├írios para portugu├¬s (mantendo AAA em ingl├¬s) -- Γ£à Separa├º├úo de classes aninhadas (LoggingConfigurationExtensionsTests, TestEvent, BenchmarkResult, BenchmarkExtensions) -- Γ£à Remo├º├úo de duplicados (DocumentExtensionsTests, EnumExtensionsTests, SearchableProviderTests) -- Γ£à GeographicRestrictionMiddlewareTests movido para Unit/Middleware/ -- Γ£à TestPerformanceBenchmark: classes internas separadas -- Γ£à 11 commits de refatoramento com build verde - -**Γ£à Corre├º├úo PostGIS Integration Tests** (20 Dez 2025): -- Γ£à Imagem Docker atualizada: postgres:15-alpine ΓåÆ postgis/postgis:15-3.4 -- Γ£à EnsurePostGisExtensionAsync() implementado em fixtures -- Γ£à Connection string com 'Include Error Detail=true' para diagn├│stico -- Γ£à Suporte completo a dados geogr├íficos (NetTopologySuite/GeoPoint) -- Γ£à Migrations SearchProviders agora passam na pipeline - -**Resumo da Sprint**: -- Γ£à 15 commits com melhorias significativas -- Γ£à Todos TODOs cr├¡ticos resolvidos -- Γ£à Testes melhorados (Provider Repository, Azurite) -- Γ£à Messaging refatorado (IRabbitMqInfrastructureManager extra├¡do) -- Γ£à Extensions consolidadas (BusinessMetricsMiddleware) -- Γ£à Upload file size configur├ível (IOptions pattern) -- Γ£à Build sem warnings (0 warnings) -- Γ£à Documenta├º├úo atualizada (architecture.md, configuration.md) -- Γ£à Code review aplicado (logs em ingl├¬s, path matching preciso, XML docs) - -**Atividades Planejadas** (14 tarefas principais): - -**1. Resolu├º├úo de TODOs Cr├¡ticos (Alta Prioridade)** - Γ£à 8-12h CONCLU├ìDO -- [x] IBGE Middleware Fallback - Fix validation when IBGE fails (3 TODOs em IbgeUnavailabilityTests.cs) Γ£à -- [x] Rate Limiting Cache Cleanup - Memory leak prevention (MaxPatternCacheSize=1000) Γ£à -- [x] Email Constraint Database Fix - Schema issue (clarified as not-yet-implemented) Γ£à -- [x] Azurite/Blob Storage - Container auto-creation with thread-safe initialization Γ£à -- [x] Provider Repository Tests - Documentation updated (unit vs integration) Γ£à -- [x] BusinessMetrics - Already extracted (no action needed) Γ£à -- [x] Monitoring - Structure already adequate (no action needed) Γ£à -- [x] Middleware UseSharedServices Alignment - TODO #249 RESOLVIDO Γ£à (19 Dez 2025) -- [x] Azurite Integration Tests - Configured deterministic blob storage tests Γ£à (19 Dez 2025) - -**2. Melhorias de Testes (M├⌐dia Prioridade)** - 4-6h -- [x] Testes Infrastructure Extensions - RESOLVIDO: n├úo aplic├ível Γ£à (19 Dez 2025) - - Extensions de configura├º├úo (Keycloak/PostgreSQL/Migration) validadas implicitamente em E2E/integra├º├úo - - Testes unit├írios teriam baixo ROI (mockaria apenas chamadas de configura├º├úo) - - Infraestrutura validada quando AppHost sobe e containers inicializam -- [x] Provider Repository Tests - Duplica├º├úo RESOLVIDA Γ£à (19 Dez 2025) - - Removidos testes unit├írios com mocks (260 linhas redundantes) - - Adicionados 5 testes de integra├º├úo faltantes (DeleteAsync, GetByIdsAsync, ExistsByUserIdAsync) - - 17 testes de integra├º├úo com valida├º├úo REAL de persist├¬ncia - - Redu├º├úo de manuten├º├úo + maior confian├ºa nos testes - -**3. Refatora├º├úo MeAjudaAi.Shared.Messaging** - 8-10h -- [x] ~~Separar NoOpDeadLetterService em arquivo pr├│prio~~ Γ£à CONCLU├ìDO (19 Dez 2025) -- [x] ~~Extrair DeadLetterStatistics e FailureRate para arquivos separados~~ Γ£à CONCLU├ìDO (19 Dez 2025) -- [x] ~~Extrair IMessageRetryMiddlewareFactory, MessageRetryMiddlewareFactory, MessageRetryExtensions~~ Γ£à CONCLU├ìDO (19 Dez 2025) -- [x] ~~Todos os 1245 testes do Shared passando~~ Γ£à CONCLU├ìDO (19 Dez 2025) -- [Γ£ô] ~~Organizar Factories em pasta dedicada~~ - Γ£à CONCLU├ìDO (19 Dez 2025) - - Criada pasta `Messaging/Factories/` - - `MessageBusFactory` e `DeadLetterServiceFactory` organizados - - Interfaces e implementa├º├╡es em arquivos separados - - `EnvironmentBasedDeadLetterServiceFactory` ΓåÆ `DeadLetterServiceFactory` -- [Γ£ô] ~~Organizar Services em pasta dedicada~~ - Γ£à CONCLU├ìDO (19 Dez 2025) - - Criada pasta `Messaging/Services/` - - `ServiceBusInitializationService` movido para organiza├º├úo -- [Γ£ô] ~~Organizar Options em pasta dedicada~~ - Γ£à CONCLU├ìDO (19 Dez 2025) - - Criada pasta `Messaging/Options/` - - 4 arquivos organizados: `ServiceBusOptions`, `MessageBusOptions`, `RabbitMqOptions`, `DeadLetterOptions` - - Namespace unificado: `MeAjudaAi.Shared.Messaging.Options` -- [Γ£ô] ~~Criar IMessageBusFactory + renomear MessageBusFactory.cs ΓåÆ EnvironmentBasedMessageBusFactory.cs~~ - Γ£à CONCLU├ìDO (19 Dez 2025) - - Invertido: Criada interface `IMessageBusFactory` em arquivo pr├│prio - - Classe `EnvironmentBasedMessageBusFactory` renomeada para `MessageBusFactory` - - Movido de `NoOp/Factory/` para raiz `Messaging/` - - Um arquivo por classe seguindo SRP -- [x] Extrair IRabbitMqInfrastructureManager para arquivo separado Γ£à (19 Dez 2025) -- [ ] Adicionar Integration Events faltantes nos m├│dulos (Documents, SearchProviders, ServiceCatalogs?) - BACKLOG -- [ ] Reorganiza├º├úo geral da estrutura de pastas em Messaging - BACKLOG -- [ ] Adicionar testes unit├írios para classes de messaging - BACKLOG - -**4. Refatora├º├úo Extensions (MeAjudaAi.Shared)** - Γ£à 8h CONCLU├ìDO -- [x] ~~Padronizar Extensions: criar arquivo [FolderName]Extensions.cs por funcionalidade~~ Γ£à CONCLU├ìDO (19 Dez 2025) -- [x] Extension Members (C# 14): EnumExtensions migrado com sucesso Γ£à CONCLU├ìDO (19 Dez 2025) -- [x] BusinessMetricsMiddlewareExtensions: J├í existe em Extensions/ Γ£à CONCLU├ìDO (19 Dez 2025) -- [x] Monitoring folder consolidation: Estrutura j├í adequada Γ£à CONCLU├ìDO (19 Dez 2025) - - Consolidados: CachingExtensions, CommandsExtensions, DatabaseExtensions, EventsExtensions - - ExceptionsExtensions, LoggingExtensions, MessagingExtensions, QueriesExtensions, SerializationExtensions - - Removidos 13 arquivos obsoletos (Extensions.cs gen├⌐ricos + subpastas) - - 1245/1245 testes passando -- [x] ~~Migra├º├úo para Extension Members (C# 14)~~ Γ£à AVALIADO (19 Dez 2025) - - Γ£à Sintaxe `extension(Type receiver)` validada e funcional no .NET 10 - - Γ£à Novos recursos dispon├¡veis: extension properties, static extensions, operators - - Γ£à Documentado em `docs/architecture.md` - se├º├úo "C# 14 Features Utilizados" - - ≡ƒôï Planejamento: Agendado como ├║ltima atividade da Sprint 5.5 - - ≡ƒô¥ Recomenda├º├úo: Usar Extension Members em NOVOS c├│digos que se beneficiem de properties -- [x] Extrair BusinessMetricsMiddlewareExtensions de BusinessMetricsMiddleware.cs Γ£à (19 Dez 2025) -- [x] Consolidar Monitoring folder (MonitoringExtensions.cs ├║nico) Γ£à (19 Dez 2025) -- [ ] Revisar padr├úo de extens├╡es em todas as funcionalidades do Shared - -**5. Code Quality & Cleanup (Baixa Prioridade)** - 3-4h -- [x] Padroniza├º├úo de Records - An├ílise conclu├¡da Γ£à (19 Dez 2025) - - Property-based records: DTOs/Requests (mutabilidade com `init`) - - Positional records: Domain Events, Query/Command DTOs (imutabilidade) - - Pattern adequado ao contexto de uso -- [ ] Upload File Size Configuration - Tornar configur├ível (UploadDocumentCommandHandler.cs:90) -- [x] ~~Remover api-reference.md (redundante com ReDoc + api-spec.json)~~ Γ£à CONCLU├ìDO (19 Dez) - -**6. Testes E2E SearchProviders** - 2-3 sprints (BACKLOG) -- [ ] 15 testes E2E cobrindo cen├írios principais de busca -- [ ] Valida├º├úo de integra├º├úo IBGE API, filtros, pagina├º├úo -- [ ] Autentica├º├úo/autoriza├º├úo em todos endpoints - -**7. Review Completo de Testes** - 6-8h -- [ ] Auditoria completa de todos os arquivos em tests/ -- [ ] Identificar testes duplicados, obsoletos ou mal estruturados -- [ ] Validar coverage e identificar gaps -- [ ] Documentar padr├╡es de teste para novos contribuidores - -**8. Migra├º├úo Extension Members (C# 14) - FINAL SPRINT ACTIVITY** - Γ£à 2h CONCLU├ìDO -- [x] Migrar EnumExtensions para syntax `extension(string value)` Γ£à -- [x] 18/18 testes passando (100% compatibilidade) Γ£à -- [x] Documentar patterns e guidelines em architecture.md Γ£à -- [x] Avaliado DocumentExtensions (n├úo adequado para extension properties) Γ£à - -**8. BDD Implementation (BACKLOG - Futuro)** - Sprint dedicado planejado -- [ ] Setup SpecFlow + Playwright.NET para acceptance tests -- [ ] Implementar 5-10 features cr├¡ticas em Gherkin (Provider Registration, Document Upload, Service Catalog) -- [ ] Integrar ao CI/CD pipeline -- [ ] Criar documenta├º├úo execut├ível com Gherkin -- **Benef├¡cio**: Testes de aceita├º├úo leg├¡veis para stakeholders e documenta├º├úo viva do sistema -- **Timing**: Implementa├º├úo prevista AP├ôS desenvolvimento do Customer App (Sprint 8+) -- **Escopo**: Testes end-to-end de fluxos completos (Frontend ΓåÆ Backend ΓåÆ APIs terceiras) -- **Foco**: Fluxos cr├¡ticos de usu├írio utilizados por Admin Portal e Customer App - -**Crit├⌐rios de Aceita├º├úo**: -- [x] Todos os 12 TODOs no c├│digo resolvidos ou documentados Γ£à -- [x] ~~Messaging refatorado com estrutura clara de pastas~~ Γ£à CONCLU├ìDO (19 Dez) -- [x] ~~Extensions consolidadas por funcionalidade~~ Γ£à CONCLU├ìDO (19 Dez) -- [x] Extension Blocks (C# 14) avaliado e implementado onde aplic├ível Γ£à (19 Dez) -- [x] Testes de infrastructure com >70% coverage (resolvido: n├úo aplic├ível) Γ£à (19 Dez) -- [x] 0 warnings no build Γ£à (19 Dez) -- [x] Documenta├º├úo t├⌐cnica atualizada Γ£à (19 Dez) - -**Estimativa Total**: 35-45 horas de trabalho t├⌐cnico (10h j├í conclu├¡das) -**Benef├¡cio**: Backend robusto e manuten├¡vel para suportar desenvolvimento do frontend Blazor - -**≡ƒô¥ Pr├│xima Atividade Recomendada**: Migra├º├úo para Extension Blocks (C# 14) - 4-6h -- Avaliar novo recurso de linguagem para melhorar organiza├º├úo de extension methods -- Migrar m├⌐todos de prop├│sito geral (PermissionExtensions, EnumExtensions) -- Manter padr├úo atual para DI extensions ([FolderName]Extensions.cs) - -**Γ£à Sprint 5.5 Completed** (19-30 Dez 2025): -- Refatora├º├úo MeAjudaAi.Shared.Messaging (Factories, Services, Options) -- Extensions padronizadas (14 arquivos consolidados) -- Extension Members (C# 14) implementado -- TODOs resolvidos (12/12 conclu├¡dos) -- Dependabot PRs fechados para regenera├º├úo -- 1245/1245 testes passando - -**ΓÅ│ Fase 2: EM ANDAMENTO** (JaneiroΓÇôMaio 2026) -Frontend React (NX Monorepo) + Mobile: -- Sprint 6: Blazor Admin Portal Setup - Γ£à CONCLU├ìDO (5 Jan 2026) -- Sprint 7: Blazor Admin Portal Features (6-24 Jan 2026) - Γ£à CONCLU├ìDO -- Sprint 7.16: Technical Debt Sprint (17-21 Jan 2026) - Γ£à CONCLU├ìDO -- Sprint 8A: Customer App (5-18 Fev 2026) - Γ£à CONCLU├ìDO -- Sprint 8B: Authentication & Onboarding (19 Fev - 4 Mar 2026) - Γ£à CONCLU├ìDO -- Sprint 8B.2: Technical Excellence & NX Monorepo (5-18 Mar 2026) - Γ£à CONCLU├ìDO -- Sprint 8C: Provider Web App (19-21 Mar 2026) - Γ£à CONCLU├ìDO -- Sprint 8D: Admin Portal Migration Blazor ΓåÆ React (2-15 Abr 2026) - ΓÅ│ Planejado -- Sprint 8E: Mobile App (16-29 Abr 2026) - ΓÅ│ Planejado -- Sprint 9: BUFFER (30 Abr - 6 Mai 2026) - ΓÅ│ Planejado -- MVP Final: 12-16 de Maio de 2026 - -**ΓÜá∩╕Å Risk Assessment**: Estimativas assumem velocidade consistente. NX Monorepo setup e Admin Migration s├úo os maiores riscos de escopo. Sprint 9 reservado como buffer de conting├¬ncia. - ---- - -## ≡ƒôû Vis├úo Geral - -O roadmap est├í organizado em **cinco fases principais** para entrega incremental de valor: - -1. **Γ£à Fase 1: Funda├º├úo (MVP Core)** - Registro de prestadores, busca geolocalizada, cat├ílogo de servi├ºos -2. **≡ƒöä Fase 1.5: Funda├º├úo T├⌐cnica** - Migration .NET 10, integra├º├úo, testes, observability -3. **≡ƒö« Fase 2: Frontend & Experi├¬ncia** - Blazor WASM Admin + Customer App -4. **≡ƒö« Fase 3: Qualidade e Monetiza├º├úo** - Sistema de avalia├º├╡es, assinaturas premium, verifica├º├úo automatizada -5. **≡ƒö« Fase 4: Experi├¬ncia e Engajamento** - Agendamentos, comunica├º├╡es, analytics avan├ºado - -A implementa├º├úo segue os princ├¡pios arquiteturais definidos em `architecture.md`: **Modular Monolith**, **DDD**, **CQRS**, e **isolamento schema-per-module**. - ---- - - -## ≡ƒôà 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-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** | 5 dias | 14 Dez - 18 Dez | Health Checks + Data Seeding | Γ£à CONCLU├ìDO (18 Dez - MERGED!) | -| **Sprint 5** | - | Sprints 3-4 | Quality Improvements | Γ£à CONCLU├ìDO ANTECIPADAMENTE | -| **Sprint 5.5** | 2 semanas | 19 Dez - 31 Dez | Refactor & Cleanup (Technical Debt) | Γ£à CONCLU├ìDO (30 Dez 2025) | -| **Sprint 6** | 1 semana | 30 Dez - 5 Jan | Blazor Admin Portal - Setup & Core | Γ£à CONCLU├ìDO (5 Jan 2026) | -| **Sprint 7** | 3 semanas | 6 - 24 Jan | Blazor Admin Portal - Features | Γ£à CONCLU├ìDO | -| **Sprint 7.16** | 1 semana | 17-21 Jan | Technical Debt Sprint | Γ£à CONCLU├ìDO | -| **Sprint 8** | 2 semanas | 5 - 18 Fev | Customer Web App (Web) | Γ£à CONCLU├ìDO | -| **Sprint 8B** | 2 semanas | 19 Fev - 4 Mar | Authentication & Onboarding | Γ£à CONCLU├ìDO | -| **Sprint 8B.2** | 2 semanas | 5-18 Mar | Technical Excellence & NX Monorepo | Γ£à CONCLU├ìDO | -| **Sprint 8C** | 3 dias | 19-21 Mar | Provider Web App | Γ£à CONCLU├ìDO | -| **Sprint 8D** | 3 semanas | 2-22 Abr | Admin Portal Migration | ΓÅ│ Planejado | -| **Sprint 9** | 2 semanas | 23 Abr - 11 Mai | **BUFFER: Polishing, Refactoring & Risk Mitigation** | ΓÅ│ Planejado | -| **MVP Launch** | 1 semana | 12-16 Mai 2026 | Final deployment & launch preparation | ≡ƒÄ» Target | - -**MVP Launch Target**: 12-16 de Maio de 2026 ≡ƒÄ» - -**Post-MVP (Fase 3+)**: Reviews, Assinaturas, Agendamentos (Abril 2026+) - ---- - -## Γ£à Fase 1: Funda├º├úo (MVP Core) - CONCLU├ìDA - -### Objetivo -Estabelecer as capacidades essenciais da plataforma: registro multi-etapas de prestadores com verifica├º├úo, busca geolocalizada e cat├ílogo de servi├ºos. - -### Status: Γ£à CONCLU├ìDA (Janeiro 2025) - -**Todos os 6 m├│dulos implementados, testados e integrados:** -1. Γ£à **Users** - Autentica├º├úo, perfis, roles -2. Γ£à **Providers** - Registro multi-etapas, verifica├º├úo, gest├úo -3. Γ£à **Documents** - Upload seguro, workflow de verifica├º├úo -4. Γ£à **Search & Discovery** - Busca geolocalizada com PostGIS -5. Γ£à **Locations** - Lookup de CEP, geocoding, valida├º├╡es -6. Γ£à **ServiceCatalogs** - Cat├ílogo hier├írquico de servi├ºos - -**Conquistas:** -- 28.69% test coverage (93/100 E2E passing, 296 unit tests) -- ΓÜá∩╕Å Coverage caiu ap├│s migration (packages.lock.json + generated code) -- APIs p├║blicas (IModuleApi) implementadas para todos m├│dulos -- Integration events funcionais entre m├│dulos -- Health checks configurados -- CI/CD pipeline completo no GitHub Actions -- Documenta├º├úo arquitetural completa + skipped tests tracker - -### 1.1. Γ£à M├│dulo Users (Conclu├¡do) -**Status**: Implementado e em produ├º├úo - -**Funcionalidades Entregues**: -- Γ£à Registro e autentica├º├úo via Keycloak (OIDC) -- Γ£à Gest├úo de perfil b├ísica -- Γ£à Sistema de roles e permiss├╡es -- Γ£à Health checks e monitoramento -- Γ£à API versionada com documenta├º├úo OpenAPI - ---- - -### 1.2. Γ£à M├│dulo Providers (Conclu├¡do) - -**Status**: Implementado e em produ├º├úo - -**Funcionalidades Entregues**: -- Γ£à Provider aggregate com estados de registro (`EProviderStatus`: Draft, PendingVerification, Active, Suspended, Rejected) -- Γ£à M├║ltiplos tipos de prestador (Individual, Company) -- Γ£à Verifica├º├úo de documentos integrada com m├│dulo Documents -- Γ£à BusinessProfile com informa├º├╡es de contato e identidade empresarial -- Γ£à Gest├úo de qualifica├º├╡es e certifica├º├╡es -- Γ£à Domain Events (`ProviderRegistered`, `ProviderVerified`, `ProviderRejected`) -- Γ£à API p├║blica (IProvidersModuleApi) para comunica├º├úo inter-m├│dulos -- Γ£à Queries por documento, cidade, estado, tipo e status de verifica├º├úo -- Γ£à Soft delete e auditoria completa - ---- - -### 1.3. Γ£à M├│dulo Documents (Conclu├¡do) - -**Status**: Implementado e em produ├º├úo - -**Funcionalidades Entregues**: -- Γ£à Upload seguro de documentos via Azure Blob Storage -- Γ£à Tipos de documento suportados: IdentityDocument, ProofOfResidence, ProfessionalLicense, BusinessLicense -- Γ£à Workflow de verifica├º├úo com estados (`EDocumentStatus`: Uploaded, PendingVerification, Verified, Rejected, Failed) -- Γ£à Integra├º├úo completa com m├│dulo Providers -- Γ£à Domain Events (`DocumentUploaded`, `DocumentVerified`, `DocumentRejected`, `DocumentFailed`) -- Γ£à API p├║blica (IDocumentsModuleApi) para queries de documentos -- Γ£à Verifica├º├╡es de integridade: HasVerifiedDocuments, HasRequiredDocuments, HasPendingDocuments -- Γ£à Sistema de contadores por status (DocumentStatusCountDto) -- Γ£à Suporte a OCR data extraction (campo OcrData para dados extra├¡dos) -- Γ£à Rejection/Failure reasons para auditoria - -**Arquitetura Implementada**: -```csharp -// Document: Aggregate Root -public sealed class Document : AggregateRoot -{ - public Guid ProviderId { get; } - public EDocumentType DocumentType { get; } - public string FileUrl { get; } // Blob name/key no Azure Storage - public string FileName { get; } - public EDocumentStatus Status { get; } - public DateTime UploadedAt { get; } - public DateTime? VerifiedAt { get; } - public string? RejectionReason { get; } - public string? OcrData { get; } -} -``` - -**API P├║blica Implementada**: -```csharp -public interface IDocumentsModuleApi : IModuleApi -{ - Task> GetDocumentByIdAsync(Guid documentId, CancellationToken ct = default); - Task>> GetProviderDocumentsAsync(Guid providerId, CancellationToken ct = default); - Task> GetDocumentStatusAsync(Guid documentId, CancellationToken ct = default); - Task> HasVerifiedDocumentsAsync(Guid providerId, CancellationToken ct = default); - Task> HasRequiredDocumentsAsync(Guid providerId, CancellationToken ct = default); - Task> GetDocumentStatusCountAsync(Guid providerId, CancellationToken ct = default); - Task> HasPendingDocumentsAsync(Guid providerId, CancellationToken ct = default); - Task> HasRejectedDocumentsAsync(Guid providerId, CancellationToken ct = default); -} -``` - -**Pr├│ximas Melhorias (Fase 2)**: -- ≡ƒöä Background worker para verifica├º├úo automatizada via OCR -- ≡ƒöä Integra├º├úo com APIs governamentais para valida├º├úo -- ≡ƒöä Sistema de scoring autom├ítico baseado em qualidade de documentos - ---- - -### 1.4. Γ£à M├│dulo Search & Discovery (Conclu├¡do) - -**Status**: Implementado e em produ├º├úo - -**Funcionalidades Entregues**: -- Γ£à Busca geolocalizada com PostGIS nativo -- Γ£à Read model denormalizado otimizado (SearchableProvider) -- Γ£à Filtros por raio, servi├ºos, rating m├¡nimo e subscription tiers -- Γ£à Ranking multi-crit├⌐rio (tier ΓåÆ rating ΓåÆ dist├óncia) -- Γ£à Pagina├º├úo server-side com contagem total -- Γ£à Queries espaciais nativas (ST_DWithin, ST_Distance) -- Γ£à Hybrid repository (EF Core + Dapper) para performance -- Γ£à Valida├º├úo de raio n├úo-positivo (short-circuit) -- Γ£à CancellationToken support para queries longas -- Γ£à API p├║blica (ISearchModuleApi) - -**Arquitetura Implementada**: -```csharp -// SearchableProvider: Read Model -public sealed class SearchableProvider : AggregateRoot -{ - public Guid ProviderId { get; } - public string Name { get; } - public GeoPoint Location { get; } // Latitude, Longitude com PostGIS - public decimal AverageRating { get; } - public int TotalReviews { get; } - public ESubscriptionTier SubscriptionTier { get; } // Free, Standard, Gold, Platinum - public Guid[] ServiceIds { get; } - public bool IsActive { get; } - public string? Description { get; } - public string? City { get; } - public string? State { get; } -} -``` - -**API P├║blica Implementada**: -```csharp -public interface ISearchModuleApi -{ - Task> SearchProvidersAsync( - double latitude, - double longitude, - double radiusInKm, - Guid[]? serviceIds = null, - decimal? minRating = null, - SubscriptionTier[]? subscriptionTiers = null, - int pageNumber = 1, - int pageSize = 20, - CancellationToken cancellationToken = default); -} -``` - -**L├│gica de Ranking Implementada**: -1. Γ£à Filtrar por raio usando `ST_DWithin` (├¡ndice GIST) -2. Γ£à Ordenar por tier de assinatura (Platinum > Gold > Standard > Free) -3. Γ£à Ordenar por avalia├º├úo m├⌐dia (descendente) -4. Γ£à Ordenar por dist├óncia (crescente) como desempate - -**Performance**: -- Γ£à Queries espaciais executadas no banco (n├úo in-memory) -- Γ£à ├ìndices GIST para geolocaliza├º├úo -- Γ£à Pagina├º├úo eficiente com OFFSET/LIMIT -- Γ£à Count query separada para total - -**Pr├│ximas Melhorias (Opcional)**: -- ≡ƒöä Migra├º├úo para Elasticsearch para maior escalabilidade (se necess├írio) -- ≡ƒöä Indexing worker consumindo integration events (atualmente manual) -- ≡ƒöä Caching de resultados para queries frequentes - ---- - -### 1.5. Γ£à M├│dulo Location Management (Conclu├¡do) - -**Status**: Implementado e testado com integra├º├úo IBGE ativa - -**Objetivo**: Abstrair funcionalidades de geolocaliza├º├úo e lookup de CEP brasileiro. - -**Funcionalidades Entregues**: -- Γ£à ValueObjects: Cep, Coordinates, Address com valida├º├úo completa -- Γ£à Integra├º├úo com APIs de CEP: ViaCEP, BrasilAPI, OpenCEP -- Γ£à Fallback chain autom├ítico (ViaCEP ΓåÆ BrasilAPI ΓåÆ OpenCEP) -- Γ£à Resili├¬ncia HTTP via ServiceDefaults (retry, circuit breaker, timeout) -- Γ£à API p├║blica (ILocationModuleApi) para comunica├º├úo inter-m├│dulos -- Γ£à **Integra├º├úo IBGE API** (Sprint 1 Dia 1): Valida├º├úo geogr├ífica oficial -- Γ£à Servi├ºo de geocoding (stub para implementa├º├úo futura) -- Γ£à 52 testes unit├írios passando (100% coverage em ValueObjects) - -**Arquitetura Implementada**: -```csharp -// ValueObjects -public sealed class Cep // Valida e formata CEP brasileiro (12345-678) -public sealed class Coordinates // Latitude/Longitude com valida├º├úo de limites -public sealed class Address // Endere├ºo completo com CEP, rua, bairro, cidade, UF - -// API P├║blica -public interface ILocationModuleApi : IModuleApi -{ - Task> GetAddressFromCepAsync(string cep, CancellationToken ct = default); - Task> GetCoordinatesFromAddressAsync(string address, CancellationToken ct = default); -} -``` - -**Servi├ºos Implementados**: -- `CepLookupService`: Implementa chain of responsibility com fallback entre provedores -- `ViaCepClient`, `BrasilApiCepClient`, `OpenCepClient`: Clients HTTP com resili├¬ncia -- **`IbgeClient`** (Novo): Cliente HTTP para IBGE Localidades API com normaliza├º├úo de nomes -- **`IbgeService`** (Novo): Valida├º├úo de munic├¡pios com HybridCache (7 dias TTL) -- **`GeographicValidationService`** (Novo): Adapter pattern para integra├º├úo com middleware -- `GeocodingService`: Stub (TODO: integra├º├úo com Nominatim ou Google Maps API) - -**Integra├º├úo IBGE Implementada** (Sprint 1 Dia 1): -```csharp -// IbgeClient: Normaliza├º├úo de nomes (remove acentos, lowercase, h├¡fens) -public Task GetMunicipioByNameAsync(string cityName, CancellationToken ct = default); -public Task> GetMunicipiosByUFAsync(string ufSigla, CancellationToken ct = default); -public Task ValidateCityInStateAsync(string city, string state, CancellationToken ct = default); - -// IbgeService: Business logic com cache (HybridCache, TTL: 7 dias) -public Task ValidateCityInAllowedRegionsAsync( - string cityName, - string stateSigla, - List allowedCities, - CancellationToken ct = default); -public Task GetCityDetailsAsync(string cityName, CancellationToken ct = default); - -// GeographicValidationService: Adapter para Shared module -public Task ValidateCityAsync( - string cityName, - string stateSigla, - List allowedCities, - CancellationToken ct = default); -``` - -**Observa├º├úo**: IBGE integration provides city/state validation for geographic restriction; geocoding (lat/lon lookup) via Nominatim is planned for Sprint 3 (optional improvement). - -**Modelos IBGE**: -- `Regiao`: Norte, Nordeste, Sudeste, Sul, Centro-Oeste -- `UF`: Unidade da Federa├º├úo (estado) com regi├úo -- `Mesorregiao`: Mesorregi├úo com UF -- `Microrregiao`: Microrregi├úo com mesorregi├úo -- `Municipio`: Munic├¡pio com hierarquia completa + helper methods (GetUF, GetEstadoSigla, GetNomeCompleto) - -**API Base IBGE**: `https://servicodados.ibge.gov.br/api/v1/localidades/` - -**Pr├│ximas Melhorias (Opcional)**: -- ≡ƒöä Implementar GeocodingService com Nominatim (OpenStreetMap) ou Google Maps API -- ≡ƒöä Adicionar caching Redis para reduzir chamadas ├ás APIs externas (TTL: 24h para CEP, 7d para geocoding) -- Γ£à ~~Integra├º├úo com IBGE para lookup de munic├¡pios e estados~~ (IMPLEMENTADO) - ---- - -### 1.6. Γ£à M├│dulo ServiceCatalogs (Conclu├¡do) - -**Status**: Implementado e funcional com testes completos - -**Objetivo**: Gerenciar tipos de servi├ºos que prestadores podem oferecer por cat├ílogo gerenciado administrativamente. - -#### **Arquitetura Implementada** -- **Padr├úo**: DDD + CQRS com hierarquia de categorias -- **Schema**: `service_catalogs` (isolado) -- **Naming**: snake_case no banco, PascalCase no c├│digo - -#### **Entidades de Dom├¡nio Implementadas** -```csharp -// ServiceCategory: Aggregate Root -public sealed class ServiceCategory : AggregateRoot -{ - public string Name { get; } - public string? Description { get; } - public bool IsActive { get; } - public int DisplayOrder { get; } - - // Domain Events: Created, Updated, Activated, Deactivated - // Business Rules: Nome ├║nico, valida├º├╡es de cria├º├úo/atualiza├º├úo -} - -// Service: Aggregate Root -public sealed class Service : AggregateRoot -{ - public ServiceCategoryId CategoryId { get; } - public string Name { get; } - public string? Description { get; } - public bool IsActive { get; } - public int DisplayOrder { get; } - - // Domain Events: Created, Updated, Activated, Deactivated, CategoryChanged - // Business Rules: Nome ├║nico, categoria ativa, valida├º├╡es -} -``` - -#### **Camadas Implementadas** - -**1. Domain Layer** Γ£à -- `ServiceCategoryId` e `ServiceId` (strongly-typed IDs) -- Agregados com l├│gica de neg├│cio completa -- 9 Domain Events (lifecycle completo) -- Reposit├│rios: `IServiceCategoryRepository`, `IServiceRepository` -- Exception: `CatalogDomainException` - -**2. Application Layer** Γ£à -- **DTOs**: ServiceCategoryDto, ServiceDto, ServiceListDto, ServiceCategoryWithCountDto -- **Commands** (11 total): - - Categories: Create, Update, Activate, Deactivate, Delete - - Services: Create, Update, ChangeCategory, Activate, Deactivate, Delete -- **Queries** (6 total): - - Categories: GetById, GetAll, GetWithCount - - Services: GetById, GetAll, GetByCategory -- **Handlers**: 11 Command Handlers + 6 Query Handlers -- **Module API**: `ServiceCatalogsModuleApi` para comunica├º├úo inter-m├│dulos - -**3. Infrastructure Layer** Γ£à -- `ServiceCatalogsDbContext` com schema isolation (`service_catalogs`) -- EF Core Configurations (snake_case, ├¡ndices otimizados) -- Repositories com SaveChangesAsync integrado -- DI registration com auto-migration support - -**4. API Layer** Γ£à -- **Endpoints REST** usando Minimal APIs pattern: - - `GET /api/v1/service-catalogs/categories` - Listar categorias - - `GET /api/v1/service-catalogs/categories/{id}` - Buscar categoria - - `POST /api/v1/service-catalogs/categories` - Criar categoria - - `PUT /api/v1/service-catalogs/categories/{id}` - Atualizar categoria - - `POST /api/v1/service-catalogs/categories/{id}/activate` - Ativar - - `POST /api/v1/service-catalogs/categories/{id}/deactivate` - Desativar - - `DELETE /api/v1/service-catalogs/categories/{id}` - Deletar - - `GET /api/v1/service-catalogs/services` - Listar servi├ºos - - `GET /api/v1/service-catalogs/services/{id}` - Buscar servi├ºo - - `GET /api/v1/service-catalogs/services/category/{categoryId}` - Por categoria - - `POST /api/v1/service-catalogs/services` - Criar servi├ºo - - `PUT /api/v1/service-catalogs/services/{id}` - Atualizar servi├ºo - - `POST /api/v1/service-catalogs/services/{id}/change-category` - Mudar categoria - - `POST /api/v1/service-catalogs/services/{id}/activate` - Ativar - - `POST /api/v1/service-catalogs/services/{id}/deactivate` - Desativar - - `DELETE /api/v1/service-catalogs/services/{id}` - Deletar -- **Autoriza├º├úo**: Todos endpoints requerem role Admin -- **Versionamento**: Sistema unificado via BaseEndpoint - -**5. Shared.Contracts** Γ£à -- `IServiceCatalogsModuleApi` - Interface p├║blica -- DTOs: ModuleServiceCategoryDto, ModuleServiceDto, ModuleServiceListDto, ModuleServiceValidationResultDto - -#### **API P├║blica Implementada** -```csharp -public interface IServiceCatalogsModuleApi : IModuleApi -{ - Task> GetServiceCategoryByIdAsync(Guid categoryId, CancellationToken ct = default); - Task>> GetAllServiceCategoriesAsync(bool activeOnly = true, CancellationToken ct = default); - Task> GetServiceByIdAsync(Guid serviceId, CancellationToken ct = default); - Task>> GetAllServicesAsync(bool activeOnly = true, CancellationToken ct = default); - Task>> GetServicesByCategoryAsync(Guid categoryId, bool activeOnly = true, CancellationToken ct = default); - Task> IsServiceActiveAsync(Guid serviceId, CancellationToken ct = default); - Task> ValidateServicesAsync(Guid[] serviceIds, CancellationToken ct = default); -} -``` - -#### **Status de Compila├º├úo** -- Γ£à **Domain**: BUILD SUCCEEDED (3 warnings XML documentation) -- Γ£à **Application**: BUILD SUCCEEDED (18 warnings SonarLint - n├úo cr├¡ticos) -- Γ£à **Infrastructure**: BUILD SUCCEEDED -- Γ£à **API**: BUILD SUCCEEDED -- Γ£à **Adicionado ├á Solution**: 4 projetos integrados - -#### **Integra├º├úo com Outros M├│dulos** -- **Providers Module** (Planejado): Adicionar ProviderServices linking table -- **Search Module** (Planejado): Denormalizar services nos SearchableProvider -- **Admin Portal**: Endpoints prontos para gest├úo de cat├ílogo - -#### **Pr├│ximos Passos (P├│s-MVP)** -1. **Testes**: Implementar unit tests e integration tests -2. **Migrations**: Criar e aplicar migration inicial do schema `service_catalogs` -3. **Bootstrap**: Integrar no Program.cs e AppHost -4. **Provider Integration**: Estender Providers para suportar ProviderServices -5. **Admin UI**: Interface para gest├úo de cat├ílogo -6. **Seeders**: Popular cat├ílogo inicial com servi├ºos comuns - -#### **Considera├º├╡es T├⌐cnicas** -- **SaveChangesAsync**: Integrado nos reposit├│rios (padr├úo do projeto) -- **Valida├º├╡es**: Nome ├║nico por categoria/servi├ºo, categoria ativa para criar servi├ºo -- **Soft Delete**: N├úo implementado (hard delete com valida├º├úo de depend├¬ncias) -- **Cascata**: DeleteServiceCategory valida se h├í servi├ºos vinculados - -#### **Schema do Banco de Dados** -```sql --- Schema: service_catalogs -CREATE TABLE service_catalogs.service_categories ( - id UUID PRIMARY KEY, - name VARCHAR(200) NOT NULL UNIQUE, - description TEXT, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - display_order INT NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP -); - -CREATE TABLE service_catalogs.services ( - id UUID PRIMARY KEY, - category_id UUID NOT NULL REFERENCES service_catalogs.service_categories(id), - name VARCHAR(200) NOT NULL UNIQUE, - description TEXT, - is_active BOOLEAN NOT NULL DEFAULT TRUE, - display_order INT NOT NULL DEFAULT 0, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP -); - -CREATE INDEX idx_services_category_id ON service_catalogs.services(category_id); -CREATE INDEX idx_services_is_active ON service_catalogs.services(is_active); -CREATE INDEX idx_service_categories_is_active ON service_catalogs.service_categories(is_active); -``` - ---- - -## ≡ƒöä Fase 1.5: Funda├º├úo T├⌐cnica (Em Andamento) - -### Objetivo -Fortalecer a base t├⌐cnica do sistema antes de desenvolver frontend, garantindo escalabilidade, qualidade e compatibilidade com .NET 10 LTS + Aspire 13. - -### Justificativa -Com todos os 6 m├│dulos core implementados (Fase 1 Γ£à), precisamos consolidar a funda├º├úo t├⌐cnica antes de iniciar desenvolvimento frontend: -- **.NET 9 EOL**: Suporte expira em maio 2025, migrar para .NET 10 LTS agora evita migra├º├úo em produ├º├úo -- **Aspire 13**: Novas features de observability e orchestration -- **Test Coverage**: Atual 40.51% ΓåÆ objetivo 80%+ para manutenibilidade -- **Integra├º├úo de M├│dulos**: IModuleApi implementado mas n├úo utilizado com as regras de neg├│cio reais -- **Restri├º├úo Geogr├ífica**: MVP exige opera├º├úo apenas em cidades piloto (SP, RJ, BH) - ---- - -### ≡ƒôà Sprint 0: Migration .NET 10 + Aspire 13 (1-2 semanas) - -**Status**: Γ£à CONCLU├ìDO (10 Dez 2025) - Branch: `improve-tests-coverage-2` - -**Objetivos**: -- Migrar todos projetos para .NET 10 LTS -- Atualizar Aspire para v13 -- Atualizar depend├¬ncias (EF Core 10, Npgsql 10, etc.) -- Validar testes e corrigir breaking changes -- Atualizar CI/CD para usar .NET 10 SDK - -**Tarefas**: -- [x] Criar branch `migration-to-dotnet-10` Γ£à -- [x] Merge master (todos m├│dulos Fase 1) Γ£à -- [x] Atualizar `Directory.Packages.props` para .NET 10 Γ£à -- [x] Atualizar todos `.csproj` para `net10.0` Γ£à -- [x] Atualizar Aspire packages para v13.0.2 Γ£à -- [x] Atualizar EF Core para 10.0.1 GA Γ£à -- [x] Atualizar Npgsql para 10.0.0 GA Γ£à -- [x] `dotnet restore` executado com sucesso Γ£à -- [x] **Verifica├º├úo Incremental**: - - [x] Build Domain projects ΓåÆ Γ£à sem erros - - [x] Build Application projects ΓåÆ Γ£à sem erros - - [x] Build Infrastructure projects ΓåÆ Γ£à sem erros - - [x] Build API projects ΓåÆ Γ£à sem erros - - [x] Build completo ΓåÆ Γ£à 0 warnings, 0 errors - - [x] Fix testes Hangfire (Skip para CI/CD) Γ£à - - [x] Run unit tests ΓåÆ Γ£à 480 testes (479 passed, 1 skipped) - - [x] Run integration tests ΓåÆ Γ£à validados com Docker -- [x] Atualizar CI/CD workflows (removido --locked-mode) Γ£à -- [x] Validar Docker images com .NET 10 Γ£à -- [x] Merge para master ap├│s valida├º├úo completa Γ£à - -**Resultado Alcan├ºado**: -- Γ£à Sistema rodando em .NET 10 LTS com Aspire 13.0.2 -- Γ£à Todos 480 testes passando (479 passed, 1 skipped) -- Γ£à CI/CD funcional (GitHub Actions atualizado) -- Γ£à Documenta├º├úo atualizada -- Γ£à EF Core 10.0.1 GA + Npgsql 10.0.0 GA (vers├╡es est├íveis) - -#### ≡ƒôª Pacotes com Vers├╡es N├úo-Est├íveis ou Pendentes de Atualiza├º├úo - -ΓÜá∩╕Å **CRITICAL**: All packages listed below are Release Candidate (RC) or Preview versions. -**DO NOT deploy to production** until stable versions are released. See [.NET 10 Release Timeline](https://github.com/dotnet/core/releases). - -**Status da Migration**: A maioria dos pacotes core j├í est├í em .NET 10, mas alguns ainda est├úo em **RC (Release Candidate)** ou aguardando releases est├íveis. - -**Pacotes Atualizados (RC/Preview)**: -```xml - - - - - - - - - - - - - - - - - -``` - -**≡ƒôª Pacotes Atualizados ΓÇö Estado Misto (11 Dez 2025)**: - -| Pacote | Vers├úo Atual | Status | Notas | -|--------|--------------|--------|-------| -| **EF Core 10.x** | `10.0.1` | Γ£à GA STABLE | Atualizado de 10.0.0-rc.2 ΓåÆ 10.0.1 GA | -| **Npgsql 10.x** | `10.0.0` | Γ£à GA STABLE | Atualizado de 10.0.0-rc.1 ΓåÆ 10.0.0 GA | -| **Aspire 13.x** | `13.0.2` | Γ£à GA STABLE | Atualizado de 13.0.0-preview.1 ΓåÆ 13.0.2 GA | -| **Aspire.Npgsql.EntityFrameworkCore.PostgreSQL** | `13.0.2` | Γ£à GA STABLE | Sincronizado com Aspire 13.0.2 GA | -| **Hangfire.PostgreSql** | `1.20.13` | ΓÜá∩╕Å STABLE (Npgsql 6.x) | Monitorando compatibilidade com Npgsql 10.x | -| **EFCore.NamingConventions** | `10.0.0-rc.2` | ΓÜá∩╕Å PRE-RELEASE | Aguardando vers├úo est├ível (issue template criado) | - -**≡ƒåò Atualiza├º├╡es via Dependabot (11 Dez 2025)**: - -| Pacote | Vers├úo Anterior | Vers├úo Atual | PR | Status | -|--------|-----------------|--------------|-----|--------| -| **Microsoft.AspNetCore.Authentication.JwtBearer** | `10.0.0` | `10.0.1` | [#62](https://github.com/frigini/MeAjudaAi/pull/62) | Γ£à MERGED | -| **Microsoft.AspNetCore.OpenApi** | `10.0.0` | `10.0.1` | [#64](https://github.com/frigini/MeAjudaAi/pull/64) | Γ£à MERGED | -| **Microsoft.Extensions.Caching.Hybrid** | `10.0.0` | `10.1.0` | [#63](https://github.com/frigini/MeAjudaAi/pull/63) | Γ£à MERGED | -| **Microsoft.Extensions.Http.Resilience** | `10.0.0` | `10.1.0` | [#63](https://github.com/frigini/MeAjudaAi/pull/63) | Γ£à MERGED | -| **Serilog** | `4.2.0` | `4.3.0` | [#63](https://github.com/frigini/MeAjudaAi/pull/63) | Γ£à MERGED | -| **Serilog.Sinks.Console** | `6.0.0` | `6.1.1` | [#63](https://github.com/frigini/MeAjudaAi/pull/63) | Γ£à MERGED | - -**Γ£à Resultado**: Pacotes core (EF Core 10.0.1, Npgsql 10.0.0, Aspire 13.0.2) atualizados para GA est├íveis. EFCore.NamingConventions 10.0.0-rc.2 sob monitoramento (aguardando GA). Lockfiles regenerados e validados em CI/CD. - -**ΓÜá∩╕Å Pacotes Ainda a Monitorar**: - -| Pacote | Vers├úo Atual | Vers├úo Est├ível Esperada | Impacto | A├º├úo Requerida | -|--------|--------------|-------------------------|---------|----------------| -| **EFCore.NamingConventions** | `10.0.0-rc.2` | `10.0.0` (Q1 2026?) | M├ëDIO | Monitorar | -| **Hangfire.PostgreSql** | `1.20.13` | `2.0.0` com Npgsql 10+ | CR├ìTICO | Monitorar | - -**≡ƒöö Monitoramento Autom├ítico de Releases**: - -Para receber notifica├º├╡es quando novas vers├╡es est├íveis forem lan├ºadas, configure os seguintes alertas: - -1. **GitHub Watch (Reposit├│rios Open Source)**: - - Acesse: ΓåÆ Click "Watch" ΓåÆ "Custom" ΓåÆ "Releases" - - Acesse: ΓåÆ Click "Watch" ΓåÆ "Custom" ΓåÆ "Releases" - - Acesse: ΓåÆ Click "Watch" ΓåÆ "Custom" ΓåÆ "Releases" - - Acesse: ΓåÆ Click "Watch" ΓåÆ "Custom" ΓåÆ "Releases" - - **Benef├¡cio**: Notifica├º├úo no GitHub e email quando nova release for publicada - -2. **NuGet Package Monitoring (Via GitHub Dependabot)**: - - Criar `.github/dependabot.yml` no reposit├│rio: - ```yaml - version: 2 - updates: - - package-ecosystem: "nuget" - directory: "/" - schedule: - interval: "weekly" - open-pull-requests-limit: 10 - # Ignorar vers├╡es preview/rc se desejar apenas stable - ignore: - - dependency-name: "*" - update-types: ["version-update:semver-major"] - ``` - - **Benef├¡cio**: PRs autom├íticos quando novas vers├╡es forem detectadas - -3. **NuGet.org Email Notifications**: - - Acesse: ΓåÆ "Change Email Preferences" - - Habilite "Package update notifications" - - **Limita├º├úo**: N├úo funciona para todos pacotes, depende do publisher - -4. **Visual Studio / Rider IDE Alerts**: - - **Visual Studio**: Tools ΓåÆ Options ΓåÆ NuGet Package Manager ΓåÆ "Check for updates automatically" - - **Rider**: Settings ΓåÆ Build, Execution, Deployment ΓåÆ NuGet ΓåÆ "Check for package updates" - - **Benef├¡cio**: Notifica├º├úo visual no Solution Explorer - -5. **dotnet outdated (CLI Tool)**: - ```powershell - # Instalar globalmente - dotnet tool install --global dotnet-outdated-tool - - # Verificar pacotes desatualizados - dotnet outdated - - # Verificar apenas pacotes major/minor desatualizados - dotnet outdated --upgrade:Major - - # Automatizar verifica├º├úo semanal (Task Scheduler / cron) - # Windows Task Scheduler: Executar semanalmente - # C:\Code\MeAjudaAi> dotnet outdated > outdated-report.txt - ``` - - **Benef├¡cio**: Script automatizado para verifica├º├úo peri├│dica - -6. **GitHub Actions Workflow (Recomendado)**: - - Criar `.github/workflows/check-dependencies.yml`: - ```yaml - name: Check Outdated Dependencies - - on: - schedule: - - cron: '0 9 * * 1' # Toda segunda-feira ├ás 9h - workflow_dispatch: # Manual trigger - - jobs: - check-outdated: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Setup .NET - uses: actions/setup-dotnet@v5 - with: - dotnet-version: '10.x' - - - name: Install dotnet-outdated - run: dotnet tool install --global dotnet-outdated-tool - - - name: Check for outdated packages - run: | - dotnet outdated > outdated-report.txt - cat outdated-report.txt - - - name: Create Issue if outdated packages found - if: success() - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - const report = fs.readFileSync('outdated-report.txt', 'utf8'); - if (report.includes('has newer versions')) { - github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: '[AUTOMATED] Outdated NuGet Packages Detected', - body: `\`\`\`\n${report}\n\`\`\``, - labels: ['dependencies', 'automated'] - }); - } - ``` - - **Benef├¡cio**: Verifica├º├úo autom├ítica semanal + cria├º├úo de Issue no GitHub - -**≡ƒôï Checklist de Monitoramento (Recomendado)**: -- [x] Configurar GitHub Watch para dotnet/efcore Γ£à -- [x] Configurar GitHub Watch para npgsql/npgsql Γ£à -- [x] Configurar GitHub Watch para dotnet/aspire Γ£à -- [x] Configurar GitHub Watch para Hangfire.PostgreSql Γ£à -- [x] Issue template criado: `.github/ISSUE_TEMPLATE/efcore-naming-conventions-stable-monitoring.md` Γ£à -- [ ] Instalar `dotnet-outdated-tool` globalmente (opcional - monitoramento manual) -- [ ] Criar GitHub Actions workflow para verifica├º├úo autom├ítica (`.github/workflows/check-dependencies.yml`) (Sprint 3) -- [x] Dependabot habilitado via GitHub (PRs autom├íticos ativos) Γ£à -- [ ] Adicionar lembrete mensal no calend├írio para verifica├º├úo manual (backup) - -**≡ƒöì Pacotes Cr├¡ticos Sem Compatibilidade .NET 10 Confirmada**: - -1. **Hangfire.PostgreSql 1.20.12** - - **Status**: Compilado contra Npgsql 6.x - - **Risco**: Breaking changes em Npgsql 10.x n├úo validados pelo mantenedor - - **Mitiga├º├úo Atual**: Testes de integra├º├úo (marcados como Skip no CI/CD) - - **Monitoramento**: - - GitHub Issues: [Hangfire.PostgreSql Issues](https://github.com/frankhommers/Hangfire.PostgreSql/issues) - - Alternativas: Hangfire.Pro.Redis (pago), Hangfire.SqlServer (outro DB) - - **Prazo**: Validar localmente ANTES de deploy para produ├º├úo - -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**: - -```mermaid -gantt - title Roadmap de Atualiza├º├╡es de Pacotes - dateFormat YYYY-MM-DD - section EF Core - RC ΓåÆ Stable :2025-11-20, 2025-12-15 - Atualizar projeto :2025-12-15, 7d - section Npgsql - RC ΓåÆ Stable :2025-11-20, 2025-12-15 - Revalidar Hangfire :2025-12-15, 7d - section Aspire - Preview ΓåÆ Stable :2025-11-20, 2025-12-31 - Atualizar configs :2025-12-31, 3d - section Hangfire - Monitorar upstream :2025-11-20, 2026-06-30 -``` - -**Γ£à A├º├╡es Conclu├¡das P├│s-Migration (10 Dez 2025)**: -1. Γ£à Finalizar valida├º├úo de testes (unit + integration) - 480 testes passando -2. Γ£à Validar Hangfire localmente (com Aspire) - funcional -3. Γ£à Configurar GitHub Watch para monitoramento de releases (EF Core, Npgsql, Aspire) -4. Γ£à Issue template criado para EFCore.NamingConventions stable monitoring -5. Γ£à Dependabot habilitado via GitHub (PRs autom├íticos) -6. Γ£à Monitoramento ativo para Hangfire.PostgreSql 2.0 (Issue #39) - -**≡ƒô¥ Notas de Compatibilidade**: -- **EF Core 10 RC**: Sem breaking changes conhecidos desde RC.1 -- **Npgsql 10 RC**: Breaking changes documentados em -- **Aspire 13 Preview**: API est├ível, apenas features novas em desenvolvimento - ---- - -### ≡ƒôà Sprint 1: Geographic Restriction + Module Integration (10 dias) - -**Status**: ≡ƒöä DIAS 1-6 CONCLU├ìDOS | FINALIZANDO (22-25 Nov 2025) -**Branches**: `feature/geographic-restriction` (merged Γ£à), `feature/module-integration` (em review), `improve-tests-coverage` (criada) -**Documenta├º├úo**: An├ílise integrada em [testing/coverage.md](./testing/coverage.md) - -**Conquistas**: -- Γ£à Sprint 0 conclu├¡do: Migration .NET 10 + Aspire 13 merged (21 Nov) -- Γ£à Middleware de restri├º├úo geogr├ífica implementado com IBGE API integration -- Γ£à 4 Module APIs implementados (Documents, ServiceCatalogs, SearchProviders, Locations) -- Γ£à Testes reativados: 28 testes (11 AUTH + 9 IBGE + 2 ServiceCatalogs + 3 IBGE unavailability + 3 duplicates removed) -- Γ£à Skipped tests reduzidos: 20 (26%) ΓåÆ 11 (11.5%) Γ¼ç∩╕Å **-14.5%** -- Γ£à Integration events: Providers ΓåÆ SearchProviders indexing -- Γ£à Schema fixes: search_providers standardization -- Γ£à CI/CD fix: Workflow secrets validation removido - -**Objetivos Alcan├ºados**: -- Γ£à Implementar middleware de restri├º├úo geogr├ífica (compliance legal) -- Γ£à Implementar 4 Module APIs usando IModuleApi entre m├│dulos -- Γ£à Reativar 28 testes E2E skipped (auth refactor + race condition fixes) -- Γ£à Integra├º├úo cross-module: Providers Γåö Documents, Providers Γåö SearchProviders -- ΓÅ│ Aumentar coverage: 35.11% ΓåÆ 80%+ (MOVIDO PARA SPRINT 2) - -**Estrutura (2 Branches + Pr├│xima Sprint)**: - -#### Branch 1: `feature/geographic-restriction` (Dias 1-2) Γ£à CONCLU├ìDO -- [x] GeographicRestrictionMiddleware (valida├º├úo cidade/estado) Γ£à -- [x] GeographicRestrictionOptions (configuration) Γ£à -- [x] Feature toggle (Development: disabled, Production: enabled) Γ£à -- [x] Unit tests (29 tests) + Integration tests (8 tests, skipped) Γ£à -- [x] **Integra├º├úo IBGE API** (valida├º├úo oficial de munic├¡pios) Γ£à - - [x] IbgeClient com normaliza├º├úo de nomes (Muria├⌐ ΓåÆ muriae) Γ£à - - [x] IbgeService com HybridCache (7 dias TTL) Γ£à - - [x] GeographicValidationService (adapter pattern) Γ£à - - [x] 2-layer validation (IBGE primary, simple fallback) Γ£à - - [x] 15 unit tests IbgeClient Γ£à - - [x] Configura├º├úo de APIs (ViaCep, BrasilApi, OpenCep, IBGE) Γ£à - - [x] Remo├º├úo de hardcoded URLs (enforce configuration) Γ£à -- [x] **Commit**: feat(locations): Integrate IBGE API for geographic validation (520069a) Γ£à -- **Target**: 28.69% ΓåÆ 30% coverage Γ£à (CONCLU├ìDO: 92/104 testes passando) -- **Merged**: 25 Nov 2025 Γ£à - -#### Branch 2: `feature/module-integration` (Dias 3-10) Γ£à DIAS 3-6 CONCLU├ìDOS | ≡ƒöä DIA 7-10 CODE REVIEW -- [x] **Dia 3**: Refactor ConfigurableTestAuthenticationHandler (reativou 11 AUTH tests) Γ£à -- [x] **Dia 3**: Fix race conditions (identificados 2 para Sprint 2) Γ£à -- [x] **Dia 4**: IDocumentsModuleApi implementation (7 m├⌐todos) Γ£à -- [x] **Dia 5**: IServiceCatalogsModuleApi (3 m├⌐todos stub) + ISearchModuleApi (2 novos m├⌐todos) Γ£à -- [x] **Dia 6**: Integration events (Providers ΓåÆ SearchProviders indexing) Γ£à - - [x] DocumentVerifiedIntegrationEvent + handler Γ£à - - [x] ProviderActivatedIntegrationEventHandler Γ£à - - [x] SearchProviders schema fix (search ΓåÆ search_providers) Γ£à - - [x] Clean InitialCreate migration Γ£à -- [x] **Dia 7**: Naming standardization (Module APIs) Γ£à - - [x] ILocationModuleApi ΓåÆ ILocationsModuleApi Γ£à - - [x] ISearchModuleApi ΓåÆ ISearchProvidersModuleApi Γ£à - - [x] SearchModuleApi ΓåÆ SearchProvidersModuleApi Γ£à - - [x] ProviderIndexingDto ΓåÆ ModuleProviderIndexingDto Γ£à -- [x] **Dia 7**: Test cleanup (remove diagnostics) Γ£à -- [ ] **Dia 7-10**: Code review & documentation ≡ƒöä -- **Target**: 30% ΓåÆ 35% coverage, 93/100 ΓåÆ 98/100 E2E tests -- **Atual**: 2,076 tests (2,065 passing - 99.5%, 11 skipped - 0.5%) -- **Commits**: 25+ total (583 commits total na branch) -- **Status**: Aguardando code review antes de merge - -**Integra├º├╡es Implementadas**: -- Γ£à **Providers ΓåÆ Documents**: ActivateProviderCommandHandler valida documentos (4 checks) -- Γ£à **Providers ΓåÆ SearchProviders**: ProviderActivatedIntegrationEventHandler indexa providers -- Γ£à **Documents ΓåÆ Providers**: DocumentVerifiedDomainEventHandler publica integration event -- ΓÅ│ **Providers ΓåÆ ServiceCatalogs**: API criada, aguarda implementa├º├úo de gest├úo de servi├ºos -- ΓÅ│ **Providers ΓåÆ Locations**: CEP lookup (baixa prioridade) - -**Bugs Cr├¡ticos Corrigidos**: -- Γ£à AUTH Race Condition (ConfigurableTestAuthenticationHandler thread-safety) -- Γ£à IBGE Fail-Closed Bug (GeographicValidationService + IbgeService) -- Γ£à MunicipioNotFoundException criada para fallback correto -- Γ£à SearchProviders schema hardcoded (search ΓåÆ search_providers) - -#### ≡ƒåò Coverage Improvement: Γ£à CONCLU├ìDO NO SPRINT 2 -- Γ£à Coverage aumentado 28.2% ΓåÆ **90.56%** (+62.36pp - META 35% SUPERADA EM 55.56pp!) -- Γ£à 480 testes (479 passing, 1 skipped) - Suite completa validada em CI/CD -- Γ£à E2E tests para provider indexing flow implementados -- Γ£à Integration tests completos com Docker/TestContainers -- ΓÅ│ Criar .bru API collections para m├│dulos (Sprint 3) -- ΓÅ│ Atualizar tools/ projects (MigrationTool, etc.) (Sprint 3) -- **Resultado**: Sprint 2 conclu├¡do (10 Dez 2025) - Coverage report consolidado gerado - -**Tarefas Detalhadas**: - -#### 1. Integra├º├úo Providers Γåö Documents Γ£à CONCLU├ìDO -- [x] Providers: Validar `HasVerifiedDocuments` antes de aprovar prestador Γ£à -- [x] Providers: Bloquear ativa├º├úo se `HasRejectedDocuments` ou `HasPendingDocuments` Γ£à -- [x] Documents: Publicar `DocumentVerified` event para atualizar status de Providers Γ£à -- [x] Integration test: Fluxo completo de verifica├º├úo de prestador Γ£à - -#### 2. Integra├º├úo Providers Γåö ServiceCatalogs Γ£à IMPLEMENTADO -- [x] ServiceCatalogs: IServiceCatalogsModuleApi com 8 m├⌐todos implementados Γ£à -- [x] ServiceCatalogs: ValidateServicesAsync implementado Γ£à -- [x] ServiceCatalogs: Repository pattern com ServiceCategoryRepository Γ£à -- [x] Integration tests: 15 testes passando Γ£à -- ΓÅ│ Providers: Integra├º├úo de valida├º├úo de servi├ºos (Sprint 3) -- ΓÅ│ Admin Portal: UI para gest├úo de categorias/servi├ºos (Sprint 3) - -#### 3. Integra├º├úo SearchProviders Γåö Providers Γ£à CONCLU├ìDO -- [x] Search: M├⌐todos IndexProviderAsync e RemoveProviderAsync implementados Γ£à -- [x] Search: Background handler consumindo ProviderVerificationStatusUpdated events Γ£à -- [x] Search: ISearchProvidersModuleApi com 2 m├⌐todos Γ£à -- [x] Integration test: Busca retorna apenas prestadores verificados Γ£à - -#### 4. Integra├º├úo Providers Γåö Locations Γ£à IMPLEMENTADO -- [x] Locations: ILocationsModuleApi implementada Γ£à -- [x] Locations: GetAddressFromCepAsync com 3 providers (ViaCEP, BrasilAPI, OpenCEP) Γ£à -- [x] Locations: IBGE API integration para valida├º├úo de munic├¡pios Γ£à -- [x] Unit tests: 67 testes passando (Locations module) Γ£à -- ΓÅ│ Providers: Integra├º├úo autom├ítica de CEP lookup (Sprint 3) - -#### 5. Restri├º├úo Geogr├ífica (MVP Blocker) Γ£à CONCLU├ìDO -- [x] Criar `AllowedCities` configuration em appsettings Γ£à -- [x] GeographicRestrictionMiddleware implementado com IBGE integration Γ£à -- [x] Fail-open fallback para valida├º├úo simples quando IBGE unavailable Γ£à -- [x] Integration test: 24 testes passando Γ£à -- ΓÅ│ Admin: Endpoint para gerenciar cidades permitidas (Sprint 3 - GitHub Pages docs) - -**Resultado Alcan├ºado (Sprint 1)**: -- Γ£à M├│dulos integrados com business rules reais (Providers Γåö Documents, Providers Γåö SearchProviders) -- Γ£à Opera├º├úo restrita a cidades piloto configuradas (IBGE API validation) -- Γ£à Background workers consumindo integration events (ProviderActivated, DocumentVerified) -- Γ£à Valida├º├╡es cross-module funcionando (HasVerifiedDocuments, HasRejectedDocuments) -- Γ£à Naming standardization (ILocationsModuleApi, ISearchProvidersModuleApi) -- Γ£à CI/CD fix (secrets validation removido) -- Γ£à **MERGED para master** (branch improve-tests-coverage-2 ativa para continua├º├úo) - ---- - -### ≡ƒôà Sprint 2: Test Coverage Improvement - Phase 1 (2 semanas) - -**Status**: Γ£à CONCLU├ìDO em 10 Dez 2025 -**Branches**: `improve-tests-coverage` (merged Γ£à), `improve-tests-coverage-2` (ativa - branch atual) - -**Conquistas (26 Nov - 10 Dez)**: -- Γ£à **improve-tests-coverage** branch merged (39 novos testes Shared) - - Γ£à ValidationBehavior: 9 testes (+2-3% coverage) - - Γ£à TopicStrategySelector: 11 testes (+3% coverage) - - Γ£à Shared core classes: 39 unit tests total - - Γ£à Coverage pipeline habilitado para todos m├│dulos - - Γ£à Roadmap documentado com an├ílise completa de gaps -- Γ£à **improve-tests-coverage-2** branch (2 Dez 2025 - 5 commits) - - Γ£à **Task 1 - PermissionMetricsService**: Concurrency fix (Dictionary ΓåÆ ConcurrentDictionary) - - Commit: aabba3d - 813 testes passando (was 812) - - Γ£à **Task 2 - DbContext Transactions**: 10 testes criados (4 passing, 6 skipped/documented) - - Commit: 5ff84df - DbContextTransactionTests.cs (458 lines) - - Helper: ShortId() for 8-char GUIDs (Username max 30 chars) - - 6 flaky tests documented (TestContainers concurrency issues) - - ΓÅ¡∩╕Å **Task 3 - DbContextFactory**: SKIPPED (design-time only, n├úo existe em runtime) - - ΓÅ¡∩╕Å **Task 4 - SchemaIsolationInterceptor**: SKIPPED (component doesn't exist) - - Γ£à **Task 5 - Health Checks**: 47 testes totais (4 health checks cobertos) - - Commit: 88eaef8 - ExternalServicesHealthCheck (9 testes, Keycloak availability) - - Commit: 1ddbf4d - Refactor reflection removal (3 classes: internal ΓåÆ public) - - Commit: fbf02b9 - HelpProcessing (9 testes) + DatabasePerformance (9 testes) - - PerformanceHealthCheck: 20 testes (j├í existiam anteriormente) - - Γ£à **Code Quality**: Removida reflection de todos health checks (maintainability) - - Γ£à **Warning Fixes**: CA2000 reduzido de 16 ΓåÆ 5 (using statements adicionados) - - Γ£à **Shared Tests**: 841 testes passando (eram 813, +28 novos) - -**Progresso Coverage (2 Dez 2025)**: -- Baseline: 45% (antes das branches - inclu├¡a c├│digo de teste) -- **Atual: 27.9%** (14,504/51,841 lines) - **MEDI├ç├âO REAL excluindo c├│digo gerado** - - **Com c├│digo gerado**: 28.2% (14,695/52,054 lines) - diferen├ºa de -0.3% - - **C├│digo gerado exclu├¡do**: 213 linhas via ExcludeByFile patterns: - - `**/*OpenApi*.generated.cs` - - `**/System.Runtime.CompilerServices*.cs` - - `**/*RegexGenerator.g.cs` - - **An├ílise Correta**: 27.9% ├⌐ coverage do **c├│digo de produ├º├úo escrito manualmente** -- **Branch Coverage**: 21.7% (2,264/10,422 branches) - sem c├│digo gerado -- **Method Coverage**: 40.9% (2,168/5,294 m├⌐todos) - sem c├│digo gerado -- **Test Suite**: 1,407 testes totais (1,393 passing - 99.0%, 14 skipped - 1.0%, 0 failing) -- Target Phase 1: 35% (+7.1 percentage points from 27.9% baseline) -- Target Final Sprint 2: 50%+ (revised from 80% - more realistic) - -**≡ƒôè Progress├úo de Coverage - Sprint 2 (Audit Trail)**: - -| Medi├º├úo | Valor | Data | Notas | -|---------|-------|------|-------| -| **Baseline Pr├⌐-Refactor** | 28.2% | 2 Dez | Estado inicial Sprint 2 | -| **Baseline Ajustado** | 27.9% | 2 Dez | Exclus├úo c├│digo gerado (OpenAPI + Regex) | -| **P├│s-Adi├º├úo de Testes** | 90.56% | 10 Dez | 40+ novos testes + consolida├º├úo | - -**≡ƒôê Ganho Total**: +62.36 percentage points (28.2% ΓåÆ 90.56%) - -**Coverage por Assembly (Top 5 - Maiores)**: -1. **MeAjudaAi.Modules.Users.Tests**: 0% (test code, expected) -2. **MeAjudaAi.Modules.Users.Application**: 55.6% (handlers, queries, DTOs) -3. **MeAjudaAi.Modules.Users.Infrastructure**: 53.9% (Keycloak, repos, events) -4. **MeAjudaAi.Modules.Users.Domain**: 49.1% (entities, value objects, events) -5. **MeAjudaAi.Shared**: 41.2% (authorization, caching, behaviors) - -**Coverage por Assembly (Bottom 5 - Gaps Cr├¡ticos)**: -1. **MeAjudaAi.ServiceDefaults**: 20.7% (health checks, extensions) ΓÜá∩╕Å -2. **MeAjudaAi.Modules.ServiceCatalogs.Domain**: 27.6% (domain events 25-50%) -3. **MeAjudaAi.Shared.Tests**: 7.3% (test infrastructure code) -4. **MeAjudaAi.ApiService**: 55.5% (middlewares, extensions) - better than expected -5. **MeAjudaAi.Modules.Users.API**: 31.8% (endpoints, extensions) - -**Gaps Identificados (Coverage < 30%)**: -- ΓÜá∩╕Å **ServiceDefaults.HealthChecks**: 0% (ExternalServicesHealthCheck, PostgresHealthCheck, GeolocationHealth) - - **Motivo**: Classes est├úo no ServiceDefaults (AppHost), n├úo no Shared (testado) - - **A├º├úo**: Mover health checks para Shared.Monitoring ou criar testes no AppHost -- ΓÜá∩╕Å **Shared.Logging**: 0% (SerilogConfigurator, CorrelationIdEnricher, LoggingContextMiddleware) - - **A├º├úo**: Unit tests para enrichers, integration tests para middleware -- ΓÜá∩╕Å **Shared.Jobs**: 14.8% ΓåÆ **85%+** (HangfireHealthCheck, HangfireAuthorizationFilter testes criados - 20 Dez 2025) - - Γ£à **HangfireHealthCheck**: 7 unit tests (valida├º├úo de status, thresholds, null checks) - - Γ£à **HangfireAuthorizationFilter**: 11 unit tests (ACL admin, ambientes, auth checks) - - **A├º├úo Completada**: Testes unit├írios criados, coverage estimada 85%+ -- ΓÜá∩╕Å **Shared.Messaging.RabbitMq**: 12% (RabbitMqMessageBus) - - **Motivo**: Integration tests require RabbitMQ container - - **A├º├úo**: TestContainers RabbitMQ ou mocks -- ΓÜá∩╕Å **Shared.Database.Exceptions**: 17% (PostgreSqlExceptionProcessor) - - **A├º├úo**: Unit tests para constraint exception handling - -**Progresso Phase 1 (Improve-Tests-Coverage-2)**: -- Γ£à **5 Commits**: aabba3d, 5ff84df, 88eaef8, 1ddbf4d, fbf02b9 -- Γ£à **40 New Tests**: Task 2 (10 DbContext) + Task 5 (27 health checks) + Task 1 (+3 fixes) -- Γ£à **Test Success Rate**: 99.0% (1,393/1,407 passing) -- Γ£à **Build Time**: ~25 minutes (full suite with Docker integration tests) -- Γ£à **Health Checks Coverage**: - - Γ£à ExternalServicesHealthCheck: 9/9 (Shared/Monitoring) - 100% - - Γ£à HelpProcessingHealthCheck: 9/9 (Shared/Monitoring) - 100% - - Γ£à DatabasePerformanceHealthCheck: 9/9 (Shared/Monitoring) - 100% - - Γ£à PerformanceHealthCheck: 20/20 (Shared/Monitoring) - 100% (pr├⌐-existente) - - Γ¥î ServiceDefaults.HealthChecks.*: 0% (not in test scope yet) - -**Technical Decisions Validated**: -- Γ£à **No Reflection**: All health check classes changed from internal ΓåÆ public - - Reason: "N├úo ├⌐ para usar reflection, ├⌐ dif├¡cil manter c├│digo com reflection" - - Result: Direct instantiation `new MeAjudaAiHealthChecks.HealthCheckName(...)` -- Γ£à **TestContainers**: Real PostgreSQL for integration tests (no InMemory) - - Result: 4 core transaction tests passing, 6 advanced scenarios documented -- Γ£à **Moq.Protected()**: HttpMessageHandler mocking for HttpClient tests - - Result: 9 ExternalServicesHealthCheck tests passing -- Γ£à **Flaky Test Documentation**: TestContainers concurrency issues documented, not ignored - - Files: DbContextTransactionTests.cs (lines with Skip attribute + detailed explanations) - -**Phase 1 Completion** - Γ£à CONCLU├ìDO (10 Dez 2025): -- Γ£à **Coverage Report Generated**: coverage/report/index.html + Summary.txt -- Γ£à **Roadmap Update**: Documento atualizado com coverage 90.56% alcan├ºado -- Γ£à **Warnings**: Build limpo, zero warnings cr├¡ticos -- Γ£à **Merged to Master**: PR #35 merged com sucesso - -**Phase 2 Completion** - Γ£à CONCLU├ìDO (10 Dez 2025): -- Γ£à **ServiceDefaults Health Checks**: Coberto via integration tests (coverage consolidada) - - Γ£à PostgresHealthCheck: Testado via TestContainers nos m├│dulos - - Γ£à GeolocationHealthOptions: 67 testes no m├│dulo Locations - - Γ£à Health checks architecture: 47 testes em Shared/Monitoring - -- Γ£à **Logging Infrastructure**: Cobertura via testes de m├│dulos - - Γ£à Logging testado atrav├⌐s de integration tests - - Γ£à CorrelationId tracking validado em E2E tests - - Γ£à LoggingContextMiddleware: Funcional em todos m├│dulos - -- Γ£à **Messaging Resilience**: Coberto via integration events - - Γ£à Integration events: ProviderActivated, DocumentVerified testados - - Γ£à Event handlers: 15+ handlers com testes unit├írios - - Γ£à Message publishing: Validado em integration tests - -- Γ£à **Middlewares**: Testados via E2E e integration tests - - Γ£à GeographicRestrictionMiddleware: 24 integration tests - - Γ£à Authorization: Validado em 100+ E2E tests com auth - - Γ£à Request/Response pipeline: Coberto em ApiService.Tests - -- Γ£à **Database Exception Handling**: Coberto nos m├│dulos - - Γ£à Repository pattern: Testado em todos 6 m├│dulos - - Γ£à Constraint violations: Validados em integration tests - - Γ£à Transaction handling: Coberto em unit tests - -- Γ£à **Documents Module**: Implementado e testado - - Γ£à Document validation: 45+ testes unit├írios - - Γ£à DocumentRepository: Integration tests completos - - Γ£à Module API: IDocumentsModuleApi com 7 m├⌐todos testados - -**Pr├│ximas Tarefas (Sprint 3 - GitHub Pages Documentation)**: -- [ ] Migrar documenta├º├úo para MkDocs Material -- [ ] Criar .bru API collections para teste manual -- [ ] Implementar data seeding scripts -- [ ] Admin endpoints para geographic restrictions -- [ ] Finalizar integra├º├╡es cross-module pendentes - -**Objetivos Fase 1 (Dias 1-7) - Γ£à CONCLU├ìDO 2 DEZ 2025**: -- Γ£à Aumentar coverage Shared de baseline para 28.2% (medi├º├úo real) -- Γ£à Focar em componentes cr├¡ticos (Health Checks - 4/7 implementados) -- Γ£à Documentar testes flaky (6 TestContainers scope issues documented) -- Γ£à **NO REFLECTION** - todas classes public para manutenibilidade -- Γ£à 40 novos testes criados (5 commits, 1,393/1,407 passing) -- Γ£à Coverage report consolidado gerado (HTML + Text) - -**Objetivos Fase 2 (Dias 8-14) - Γ£à CONCLU├ìDO 10 DEZ 2025**: -- Γ£à ServiceDefaults: Coverage integrado ao report consolidado -- Γ£à Shared.Logging: Cobertura aumentada com testes de m├│dulos -- Γ£à Shared.Messaging: Cobertura aumentada com testes de integra├º├úo -- Γ£à Shared.Database.Exceptions: Cobertura aumentada com testes de m├│dulos -- Γ£à **Overall Target SUPERADO**: 28.2% ΓåÆ **90.56%** (+62.36 percentage points!) - -**Decis├╡es T├⌐cnicas**: -- Γ£à TestContainers para PostgreSQL (no InMemory databases) -- Γ£à Moq para HttpMessageHandler (HttpClient mocking) -- Γ£à FluentAssertions para assertions -- Γ£à xUnit 3.1.5 como framework -- Γ£à Classes public em vez de internal (no reflection needed) -- ΓÜá∩╕Å Testes flaky com concurrent scopes marcados como Skip (documentados) - -**Health Checks Implementation** - Γ£à CONCLU├ìDO: -- Γ£à **ExternalServicesHealthCheck**: Keycloak availability (9 testes - Shared/Monitoring) -- Γ£à **PerformanceHealthCheck**: Memory, GC, thread pool (20 testes - Shared/Monitoring) -- Γ£à **HelpProcessingHealthCheck**: Business logic operational (9 testes - Shared/Monitoring) -- Γ£à **DatabasePerformanceHealthCheck**: DB metrics configured (9 testes - Shared/Monitoring) -- Γ£à **ServiceDefaults.HealthChecks.PostgresHealthCheck**: Testado via TestContainers (integration tests) -- Γ£à **Locations**: APIs de CEP health validadas (67 testes - ViaCEP, BrasilAPI, IBGE, OpenCEP) -- Γ£à **Documents**: Module health validado via integration tests -- Γ£à **Search**: PostGIS testado via SearchProviders integration tests - -**Arquitetura de Health Checks** - Γ£à DEFINIDA: -- **Shared/Monitoring**: 4 health checks implementados e testados (47 testes, 100% coverage) -- **ServiceDefaults/HealthChecks**: Configura├º├╡es base para ASP.NET Core health checks -- **M├│dulos**: Cada m├│dulo com seus pr├│prios health checks espec├¡ficos -- **Decis├úo**: Arquitetura h├¡brida - Shared para componentes globais, m├│dulos para checks espec├¡ficos - -**Data Seeding** (SPRINT 3): -- [ ] Seeder de ServiceCatalogs: 10 categorias + 50 servi├ºos (estrutura pronta, dados pendentes) -- [ ] Seeder de Providers: 20 prestadores fict├¡cios -- [ ] Seeder de Users: Admin + 10 customers -- [ ] Script: `dotnet run --seed-dev-data` - -**Resultado Alcan├ºado Sprint 2 (10 Dez 2025)**: -- Γ£à **Overall coverage**: **90.56% line**, 78.2% branch, 93.4% method (Cobertura Aggregated Direct) -- Γ£à **Covered lines**: 12,487 de 14,371 coverable lines -- Γ£à **Test suite**: **480 testes** (479 passing - 99.8%, 1 skipped - 0.2%, 0 failing) -- Γ£à **Assemblies**: 25 assemblies cobertos -- Γ£à **Classes**: 528 classes, 491 files -- Γ£à **Build quality**: Zero warnings cr├¡ticos, build limpo -- Γ£à **Code quality**: Zero reflection, todas classes public -- Γ£à **Target SUPERADO**: Meta original 35% ΓåÆ **90.56% alcan├ºado** (+55.56pp acima da meta!) - - *Nota: Target Phase 2 original era 80%, revisado para 50% mid-sprint por realismo; ambos superados* -- Γ£à **CI/CD**: Todos workflows atualizados e funcionais (.NET 10 + Aspire 13) - -### Phase 2 Task Breakdown & Release Gates - Γ£à CONCLU├ìDO (10 Dez 2025) - -#### Coverage Targets (Progressive) - Γ£à SUPERADO -- ~~**Minimum (CI Warning Threshold)**: Line 70%, Branch 60%, Method 70%~~ -- ~~**Recommended**: Line 85%, Branch 75%, Method 85%~~ -- Γ£à **ALCAN├çADO**: Line **90.56%**, Branch **78.2%**, Method **93.4%** (EXCELLENT tier!) - -**Resultado**: Coverage inicial (28.2%) elevado para **90.56%** (+62.36pp). Todos os targets superados! - -#### Phase 2 Task Matrix - Γ£à TODAS TAREFAS CONCLU├ìDAS - -| Task | Priority | Estimated Tests | Target Coverage | Completed | Status | -|------|----------|-----------------|-----------------|-----------|--------| -| ServiceDefaults.HealthChecks | CRITICAL | 15-20 | 35%+ line | 10 Dez 2025 | Γ£à DONE - Testado via integration tests | -| Shared.Logging | CRITICAL | 10-12 | 30%+ line | 10 Dez 2025 | Γ£à DONE - Coberto nos m├│dulos | -| Shared.Messaging.RabbitMq | CRITICAL | 20-25 | 40%+ line | 10 Dez 2025 | Γ£à DONE - Integration events testados | -| Shared.Database.Exceptions | HIGH | 15-20 | 50%+ line | 10 Dez 2025 | Γ£à DONE - Repository pattern coberto | -| Shared.Middlewares | HIGH | 12-15 | 45%+ line | 10 Dez 2025 | Γ£à DONE - E2E tests validados | - -#### Release Gate Criteria - Γ£à TODOS CRIT├ëRIOS ATENDIDOS - -**Phase 2 Merge to Master** (Required): -- Γ£à Line Coverage: **90.56%** (target 35%+ - SUPERADO) -- Γ£à Health Checks: 100% para Shared/Monitoring (47 testes) -- Γ£à Test Suite: **480 testes** (target 1,467 - redefinido para qualidade) -- Γ£à All Tests Passing: **99.8%** (479 passing, 1 skipped) -- Γ£à Code Quality: 0 warnings cr├¡ticos, build limpo - -**Production Deployment** (Ready): -- Γ£à Critical Paths: 90%+ para todos m├│dulos (Users, Providers, Documents, etc.) -- Γ£à End-to-End Tests: Todos fluxos principais passando (E2E.Tests + Integration.Tests) -- Γ£à Performance: Health checks validados, m├⌐tricas ok -- Γ£à Security: .NET 10 GA + Aspire 13.0.2 GA (sem vulnerabilidades conhecidas) - -**Decis├úo**: Γ£à Phase 2 **MERGED para master** (PR #35) - Todos gates atendidos! - -**Decis├╡es Estrat├⌐gicas Sprint 2 - Γ£à EXECUTADAS**: -1. Γ£à **Componentes cr├¡ticos cobertos**: ServiceDefaults, Logging, Messaging - 90.56% overall -2. Γ£à **Duplica├º├úo investigada**: Arquitetura h├¡brida definida (Shared/Monitoring + m├│dulos) -3. Γ£à **TestContainers implementado**: PostgreSQL validado em 11 integration test suites -4. Γ£à **Flaky tests documentados**: 1 teste skipped (ServiceCatalogs debug), documentado -5. Γ£à **Target SUPERADO**: 90.56% alcan├ºado (original 35% + realista 80% ambos superados!) -6. Γ£à **≡ƒôÜ Documentation Hosting**: Sprint 3 iniciado - branch `migrate-docs-github-pages` criada - - Γ£à **Decis├úo confirmada**: MkDocs Material com GitHub Pages - - Γ£à **Branch criada**: 10 Dez 2025 - - **Pr├│ximos passos**: Ver se├º├úo "Sprint 3: GitHub Pages Documentation" acima - ---- - -## ≡ƒÜÇ Pr├│ximos Passos (P├│s Sprint 0 e Sprint 2) - -### 1∩╕ÅΓâú Sprint 3: Code & Documentation Organization + Final Integrations (PR├ôXIMA TAREFA) - -**Branch**: `migrate-docs-github-pages` (criada em 10 Dez 2025) -**Status**: ≡ƒöä EM PROGRESSO (Parte 1 iniciada 11 Dez 2025) -**Prioridade**: ALTA - Organiza├º├úo completa do projeto antes de prosseguir -**Estimativa**: 2-3 semanas -**Data prevista**: 11-30 Dez 2025 - -**≡ƒôà Cronograma Detalhado com Gates Semanais**: - -| Semana | Per├¡odo | Tarefa Principal | Entreg├ível | Gate de Qualidade | -|--------|---------|------------------|------------|-------------------| -| **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 - ---- - -## ≡ƒÄ» Sprint 5 (19 Dez 2025 - 3 Jan 2026) - Γ£à CONCLU├ìDA ANTECIPADAMENTE! - -**Branch**: `refactor/code-quality-standardization` - Tarefas completadas nas Sprints 3-4 - -**Status**: Γ£à TODAS as tarefas foram conclu├¡das em sprints anteriores: - -**Γ£à Prioridade 1 - Cr├¡tico (COMPLETO)**: - -1. Γ£à **Substituir NSubstitute por Moq** (Sprint 3): - - 3 arquivos migrados (ServiceDefaults.Tests, ApiService.Tests x2) - - Padroniza├º├úo completa - projeto usa 100% Moq - - Depend├¬ncia duplicada removida - -2. Γ£à **Unificar UuidGenerator** (Commit 0a448106 - Sprint 3): - - ~26 ocorr├¬ncias de `Guid.CreateVersion7()` substitu├¡das - - L├│gica centralizada em `MeAjudaAi.Shared.Time.UuidGenerator` - - Preparado para futura customiza├º├úo - -3. Γ£à **Migrar para .slnx** (Commit 1de5dc1a - Sprint 3): - - `MeAjudaAi.slnx` criado (formato XML) - - 40 projetos validados, build completo passando - - 3 workflows CI/CD atualizados (.sln ΓåÆ .slnx) - - Benef├¡cios confirmados: 5x mais r├ípido, menos conflitos git - -4. Γ£à **Design Patterns Documentation** (Sprint 3-4): - - Se├º├úo completa em `docs/architecture.md` - - Padr├╡es documentados: Repository, CQRS, Domain Events, Factory, Strategy, Middleware Pipeline - - Exemplos reais de c├│digo inclu├¡dos (AllowedCityRepository, Commands/Queries) - - Se├º├úo anti-patterns evitados adicionada - -**Γ£à Prioridade 2 - Desej├ível (COMPLETO)**: - -5. Γ£à **Bruno Collections** (Sprint 3): - - Γ£à **Users**: 6 arquivos .bru (CreateUser, DeleteUser, GetUsers, GetUserById, UpdateUser, GetUserByEmail) - - Γ£à **Providers**: 16 arquivos .bru (CRUD completo + Services + Verification) - - Γ£à **Documents**: 3 arquivos .bru (Upload, GetProviderDocuments, Verify) - - Γ£à **ServiceCatalogs**: 35+ arquivos .bru (Categories + Services CRUD) - - Γ£à **Locations**: 6 arquivos .bru (AllowedCities CRUD + README) - -**ΓÅ╕∩╕Å Tarefas Remanescentes** (Prioridade 3 - Baixa urg├¬ncia, mover para Sprint 6 ou posterior): -- ≡ƒöÆ Avaliar migra├º├úo AspNetCoreRateLimit library -- ≡ƒôè Verificar completude Logging Estruturado (Seq, Domain Events, Performance) -- ≡ƒöù Providers Γåö Locations Integration (auto-populate cidade/estado via CEP) - ---- - -## ≡ƒÄ» Pr├│ximos Passos - Sprint 6 (6 Jan - 24 Jan 2026) - -**Foco**: Frontend Blazor - Admin Portal Setup + Customer App In├¡cio - -**Branch Sugerida**: `feature/blazor-admin-portal` - -**Objetivo Geral**: Iniciar desenvolvimento frontend com Blazor WASM para Admin Portal e MAUI Hybrid para Customer App. - -**Estimativa Total**: 6-9 dias ├║teis (considerando feriados de fim de ano) - ---- - -#### ≡ƒôÜ Parte 1: Documentation Migration to GitHub Pages (1 semana) - -**Objetivos**: -- Migrar ~50 arquivos .md do diret├│rio `docs/` para GitHub Pages -- Implementar MkDocs Material para site naveg├ível -- Consolidar e eliminar documenta├º├úo duplicada/obsoleta -- Estabelecer estrutura hier├írquica l├│gica (max 3 n├¡veis) -- Deploy autom├ítico via GitHub Actions - -**Processo de Migra├º├úo** (iterativo, documento a documento): -1. **Auditoria inicial**: Listar todos os .md e categorizar (atual/defasado/duplicado) -2. **Consolida├º├úo**: Mesclar conte├║do duplicado (ex: ci-cd.md vs ci-cd/workflows-overview.md) -3. **Limpeza**: Remover informa├º├╡es obsoletas ou mover para `docs/archive/` -4. **Reorganiza├º├úo**: Estruturar hierarquia (Getting Started ΓåÆ Architecture ΓåÆ Testing ΓåÆ CI/CD ΓåÆ API) -5. **Valida├º├úo**: Revisar links internos, atualizar refer├¬ncias cruzadas -6. **Navega├º├úo**: Configurar `mkdocs.yml` com estrutura final -7. **Deploy**: Habilitar GitHub Pages e testar site completo - -**Crit├⌐rios de Qualidade**: -- Γ£à Zero duplica├º├úo de conte├║do -- Γ£à Informa├º├╡es datadas removidas ou arquivadas -- Γ£à Navega├º├úo intuitiva (max 3 n├¡veis de profundidade) -- Γ£à Todos links internos funcionando -- Γ£à Search global funcional -- Γ£à Mobile-friendly + dark mode - -**Arquivos a Criar**: -- `mkdocs.yml` (configura├º├úo principal) -- `.github/workflows/deploy-docs.yml` (CI/CD workflow) -- `docs/requirements.txt` (depend├¬ncias Python: mkdocs-material, plugins) - -**URL Final**: `https://frigini.github.io/MeAjudaAi/` - ---- - -#### ≡ƒöº Parte 2: Scripts & Tools Organization (3-4 dias) - -**Objetivos**: -- Revisar e atualizar scripts em `scripts/` -- Atualizar ferramentas em `tools/` (MigrationTool, etc.) -- Criar .bru API collections para teste manual dos m├│dulos -- Implementar data seeding scripts - -**Tarefas Detalhadas**: -- [ ] **Scripts Cleanup**: - - [ ] Revisar `scripts/generate-clean-coverage.ps1` (funcionando, documentar melhor) - - [ ] Atualizar scripts de build/deploy se necess├írio - - [ ] Criar script de data seeding: `scripts/seed-dev-data.ps1` - -- [ ] **Tools/ Projects**: - - [ ] Atualizar MigrationTool para .NET 10 - - [ ] Validar ferramentas auxiliares - - [ ] Documentar uso de cada tool - -- [ ] **API Collections (.bru)**: - - [ ] Criar collection para m├│dulo Users - - [ ] Criar collection para m├│dulo Providers - - [ ] Criar collection para m├│dulo Documents - - [ ] Criar collection para m├│dulo ServiceCatalogs - - [ ] Criar collection para m├│dulo Locations - - [ ] Criar collection para m├│dulo SearchProviders - - [ ] Documentar setup e uso das collections - -- [ ] **Data Seeding**: - - [ ] Seeder de ServiceCatalogs: 10 categorias + 50 servi├ºos - - [ ] Seeder de Providers: 20 prestadores fict├¡cios - - [ ] Seeder de Users: Admin + 10 customers - - [ ] Script: `dotnet run --seed-dev-data` - ---- - -#### ≡ƒöù Parte 3: Final Module Integrations (3-5 dias) - -**Objetivos**: -- Finalizar integra├º├╡es cross-module pendentes -- Implementar admin endpoints para gest├úo -- Validar fluxos end-to-end completos - -**Tarefas Detalhadas**: - -**1. Providers Γåö ServiceCatalogs Integration**: -- [ ] Providers: Adicionar `ProviderServices` linking table (many-to-many) -- [ ] Providers: Validar services via `IServiceCatalogsModuleApi.ValidateServicesAsync` -- [ ] Providers: Bloquear servi├ºos inativos ou inexistentes -- [ ] Integration tests: Valida├º├úo completa do fluxo - -**2. Providers Γåö Locations Integration**: -- [ ] Providers: Usar `ILocationsModuleApi.GetAddressFromCepAsync` no registro -- [ ] Providers: Auto-populate cidade/estado via Locations -- [ ] Unit test: Mock de ILocationsModuleApi em Providers.Application - -**3. Geographic Restrictions Admin**: -- Γ£à **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 -- [ ] API endpoints: CRUD de categorias e servi├ºos -- [ ] Documenta├º├úo: Workflows de gest├úo - ---- - -#### ≡ƒÄ» 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**: 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 & Monitoring** ≡ƒÅÑ: -- [x] **Health Checks Core**: Γ£à IMPLEMENTADO - - `src/Shared/Monitoring/HealthChecks.cs`: 4 health checks implementados - - 47 testes, 100% coverage - - Componentes: ExternalServicesHealthCheck, PerformanceHealthCheck, HelpProcessingHealthCheck, DatabasePerformanceHealthCheck - - Endpoints: `/health`, `/health/live`, `/health/ready` -- [x] **Dashboard**: Γ£à DECIS├âO ARQUITETURAL - - **Usar dashboard nativo do .NET Aspire** (n├úo AspNetCore.HealthChecks.UI) - - Aspire fornece dashboard integrado com telemetria, traces e m├⌐tricas - - Health checks expostos via endpoints JSON consumidos pelo Aspire - - Melhor integra├º├úo com ecossistema .NET 9+ e cloud-native deployments - - **Rationale**: Evitar depend├¬ncia extra, melhor DX, alinhamento com roadmap .NET - -**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 (implementa├º├úo pr├│pria com CommandDispatcher/QueryDispatcher) - - **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/roadmap.md b/docs/roadmap.md index 0bf5c0bac..36afaec39 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,46 +1,86 @@ -# 🗺️ Roadmap - MeAjudaAi +# 🗺️ Roadmap MeAjudaAi -Este documento consolida o planejamento estratégico e tático da plataforma MeAjudaAi. Para facilitar a leitura e manutenção, o roadmap foi dividido em seções específicas: +Este é o planejamento estratégico unificado da plataforma MeAjudaAi. --- -## 🚀 [Roadmap Atual](./roadmap-current.md) -**Status**: Fase 2 em andamento (Frontend React + Mobile). -Contém o status atual das sprints, o cronograma detalhado até o MVP e o plano de mitigação de riscos. -- **Sprint Atual**: 9 (Buffer & Risk Mitigation) -- **Sprint Concluída**: 8E (E2E Tests & React Unit Infrastructure) -- **Sprint em Andamento**: 9E (Buffer & Risk Mitigation + Módulo Comunicações) -- **Meta MVP**: Maio 2026 (12-16) +## 📊 Status Atual (Abril 2026) + +**Sprint Atual**: 10 (Qualidade & Onboarding) +**Status**: 🚧 Em andamento +**Meta MVP**: 12 - 16 de Maio de 2026 + +**Stack Principal**: .NET 10 LTS + Aspire 13 + PostgreSQL + NX Monorepo + React 19 + Next.js 15 + Tailwind v4 --- -## 📜 [Histórico do Roadmap](./roadmap-history.md) -Contém o registro de todas as fases e sprints já concluídos. -- **Fase 1**: Fundação (Janeiro 2025) -- **Fase 1.5**: Fundação Técnica (Março 2026) -- **Sprints**: 0 a 8B.1 +## 🚀 Sprint 10 - Qualidade & Onboarding (12 Abr - 26 Abr 2026) 🚧 [EM ANDAMENTO] + +**Objetivo**: Estabelecer confiança na plataforma através de avaliações e simplificar o acesso de novos prestadores. + +### 🔴 MUST-HAVE: + +#### 1. 🌟 Ratings Module (Módulo de Avaliações) ✅ [CONCLUÍDO] +* **Arquitetura**: **Consistência Eventual**. O módulo de busca (`SearchProviders`) não fará Join com o módulo de Ratings. Sempre que um review for aprovado, um `ReviewApprovedIntegrationEvent` será disparado e o módulo de busca atualizará o campo `AverageRating` no seu próprio registro desnormalizado. +* **Funcionalidades**: + * ✅ **Avaliação de Prestadores**: Clientes podem adicionar nota (1 a 5 estrelas) e comentário textual após a conclusão de um serviço. + * ✅ **Moderação de Conteúdo**: Filtro automático via Regex e manual para comentários que violem as regras (xingamentos, ofensas, SPAM). + * ✅ **Ranking de Busca**: Algoritmo de busca priorizando prestadores com melhor média e maior volume de avaliações verificadas. +* **Schema DB**: `ratings` | **ModuleName**: `Ratings`. + +#### 2. 🔑 Login Social (Instagram) - ISSUE #141 ✅ [CONCLUÍDO] +* **Ação**: Configuração de Identity Provider nativo do Instagram no Keycloak para permitir que prestadores usem seu perfil do Instagram para autenticação. + +#### 3. 🛡️ OpenAPI Breaking Change Gating (CI) ✅ [CONCLUÍDO] +* **Ação**: Novo step no workflow de PR para comparar o `api-base.json` com `api-current/api-spec.json` usando `breaking-only` e `fail-on-diff` para falhar o build caso existam mudanças destrutivas na API. Corrigido para rodar em ambiente `Testing` para evitar dependências de segredos reais. + +#### 4. 📋 Coleções Bruno (.bru) ✅ [CONCLUÍDO] +* **Ação**: Documentação técnica de 100% dos endpoints existentes em `src/Modules/*/API/API.Client/`. --- -## 🔮 [Roadmap Futuro](./roadmap-future.md) -Contém os objetivos pós-MVP e ideias para o backlog de longo prazo. -- **Fase 3**: Qualidade e Monetização -- **Fase 4**: Experiência e Engajamento +## 💰 Sprint 11 - Monetização & Polimento (27 Abr - 11 Mai 2026) [EM PLANEJAMENTO] + +**Objetivo**: Habilitar o faturamento da plataforma e finalizar a experiência do usuário. + +### 🔴 MUST-HAVE: + +#### 1. 💳 Payments Module (Módulo de Pagamentos) +* **Arquitetura**: Padrão de **Anti-Corruption Layer (ACL)**. A lógica de negócio não conhece tipos do Stripe. Abstração via `IPaymentGateway`. +* **Funcionalidades**: + * **Assinaturas de Prestadores**: Planos Free, Standard e Gold. + * **Stripe Checkout & Webhooks**: Redirecionamento seguro e processamento de eventos (`invoice.paid`, etc.). + * **Portal de Billing**: Gestão de cartões e cancelamentos. +* **Schema DB**: `payments` | **ModuleName**: `Payments`. + +#### 2. 🌍 Localização Frontend (i18n) +* **Arquitetura**: `i18next` + `react-i18next`. +* **Funcionalidades**: Suporte PT-BR/EN-US, tradução automática de erros do **Zod** e seletor de idioma na UI. + +#### 3. 🏁 Preparação para Lançamento +* **Endurecimento**: Skeletons de carregamento e mensagens de erro amigáveis em todos os fluxos. +* **Documentação Final**: Manuais de Usuário e Guias de Implantação. --- -## 📊 Sumário Executivo Atualizado +## 🔮 Roadmaps Futuros (Pós-MVP) -**Projeto**: MeAjudaAi - Plataforma de Conexão entre Clientes e Prestadores de Serviços -**Stack Principal**: .NET 10 LTS + Aspire 13 + PostgreSQL + NX Monorepo + React 19 + Next.js 15 (Customer, Provider) + Tailwind v4 -> [!NOTE] -> *Admin Portal migrado de Blazor WASM para React durante o Sprint 8D (concluído em 24 de Março de 2026).* +### Fase 3: Escala e Provedores Reais +* **Provedores de Comunicação**: Substituir Stubs por SendGrid (E-mail), Twilio (SMS) e Firebase (Push). +* **Verificação Automatizada**: OCR via Azure AI Vision e integração com APIs de antecedentes criminais. + +### Fase 4: Experiência e Engajamento +* **Módulo de Agendamentos (Bookings)**: Calendário de disponibilidade. +* **Sistema de Disputas**: Mediação administrativa para conflitos. --- -## 🏗️ Decisões Arquiteturais Recentes +## ✅ Concluído Recentemente + +* **Sprint 9**: Estabilização global, Módulo de Comunicações (Infra), Resiliência (`CancellationToken`) e Localização Backend (.resx). +* **Sprint 8D/8E**: Migração completa do Admin Portal para React e Testes E2E com Playwright. + +--- -- **NX Monorepo**: Adotado para unificar o desenvolvimento frontend e compartilhamento de código. -- **Dual-Stack Transition**: Transição de Blazor WASM para React 19 (Next.js) para unificação da stack. -- **Testing Infrastructure**: Implementação de Vitest + MSW para unitários e Playwright para E2E, com agregação de cobertura global. -- **Módulo Comunicações** (Abril 2026): Implementado durante Sprint 9 com Outbox Pattern - provedor configurável pós-MVP. +## 📜 Histórico Completo +Para detalhes das sprints anteriores, consulte o [Histórico do Roadmap](./roadmap-history.md). diff --git a/infrastructure/keycloak/realms/meajudaai-realm.dev.json b/infrastructure/keycloak/realms/meajudaai-realm.dev.json index b4ec31bde..7bf6c56af 100644 --- a/infrastructure/keycloak/realms/meajudaai-realm.dev.json +++ b/infrastructure/keycloak/realms/meajudaai-realm.dev.json @@ -1,355 +1,370 @@ { - "id": "meajudaai", - "realm": "meajudaai", - "displayName": "MeAjudaAi Development", - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": true, - "editUsernameAllowed": false, - "rememberMe": true, - "verifyEmail": false, - "bruteForceProtected": true, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "accessTokenLifespan": 1800, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "offlineSessionIdleTimeout": 2592000, - "loginTheme": "meajudaai", - "accountTheme": "meajudaai", - "emailTheme": "meajudaai", - "internationalizationEnabled": true, - "supportedLocales": [ - "pt-BR", - "en" - ], - "defaultLocale": "pt-BR", - "roles": { - "realm": [ - { - "name": "admin", - "description": "Administrador total da plataforma - todas as permissões" - }, - { - "name": "meajudaai-system-admin", - "description": "Administrador do sistema - todas as permissões" - }, - { - "name": "meajudaai-user-admin", - "description": "Administrador de usuários - gerenciar usuários" - }, - { - "name": "meajudaai-user-operator", - "description": "Operador de usuários - leitura e atualização" - }, - { - "name": "meajudaai-user", - "description": "Usuário básico - perfil próprio" - }, - { - "name": "meajudaai-provider-admin", - "description": "Administrador de prestadores - CRUD completo" - }, - { - "name": "meajudaai-provider", - "description": "Prestador - apenas leitura" - }, - { - "name": "meajudaai-order-admin", - "description": "Administrador de pedidos - CRUD completo" - }, - { - "name": "meajudaai-order-operator", - "description": "Operador de pedidos - leitura e atualização" - }, - { - "name": "meajudaai-report-admin", - "description": "Administrador de relatórios - criar e exportar" - }, - { - "name": "meajudaai-report-viewer", - "description": "Visualizador de relatórios - apenas visualizar" - }, - { - "name": "meajudaai-catalog-manager", - "description": "Gerente de catálogo - gerenciar serviços e categorias" - }, - { - "name": "meajudaai-location-manager", - "description": "Gerente de localidades - gerenciar cidades e estados" - }, - { - "name": "customer", - "description": "Cliente da plataforma" - } - ] - }, - "identityProviders": [ - { - "alias": "google", - "displayName": "Google", - "providerId": "google", - "enabled": true, - "trustEmail": true, - "storeToken": false, - "addReadTokenRoleOnCreate": false, - "authenticateByDefault": false, - "firstBrokerLoginFlowAlias": "first broker login", - "config": { - "clientId": "${GOOGLE_CLIENT_ID}", - "clientSecret": "${GOOGLE_CLIENT_SECRET}" - } - }, - { - "alias": "facebook", - "displayName": "Facebook", - "providerId": "facebook", - "enabled": true, - "trustEmail": true, - "storeToken": false, - "addReadTokenRoleOnCreate": false, - "authenticateByDefault": false, - "firstBrokerLoginFlowAlias": "first broker login", - "config": { - "clientId": "${FACEBOOK_APP_ID}", - "clientSecret": "${FACEBOOK_APP_SECRET}" - } - } - ], - "clients": [ - { - "clientId": "admin-portal", - "name": "Admin Portal - Blazor WebAssembly", - "description": "MeAjudaAi Admin Portal OIDC Client", - "enabled": true, - "publicClient": true, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "protocol": "openid-connect", - "redirectUris": [ - "http://localhost:5165/*", - "https://localhost:7032/*", - "https://localhost:7281/*", - "https://admin.meajudaai.com.br/*" - ], - "webOrigins": [ - "http://localhost:5165", - "https://localhost:7032", - "https://localhost:7281", - "https://admin.meajudaai.com.br" - ], - "frontchannelLogout": true, - "attributes": { - "pkce.code.challenge.method": "S256", - "post.logout.redirect.uris": "+" - }, - "protocolMappers": [ - { - "name": "realm-roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true", - "claim.name": "roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "clientId": "customer-app", - "name": "Customer App - Web + Mobile", - "description": "MeAjudaAi Customer App OIDC Client", - "enabled": true, - "publicClient": true, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "protocol": "openid-connect", - "redirectUris": [ - "https://localhost:7282/*", - "https://app.meajudaai.com.br/*", - "meajudaai://callback", - "http://localhost:3000/*" - ], - "webOrigins": [ - "https://localhost:7282", - "https://app.meajudaai.com.br", - "http://localhost:3000" - ], - "frontchannelLogout": true, - "attributes": { - "pkce.code.challenge.method": "S256", - "post.logout.redirect.uris": "+" - }, - "protocolMappers": [ - { - "name": "realm-roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true", - "claim.name": "roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "clientId": "provider-app", - "name": "Provider App - Web + Mobile", - "description": "MeAjudaAi Provider App OIDC Client", - "enabled": true, - "publicClient": true, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "protocol": "openid-connect", - "redirectUris": [ - "https://localhost:7283/*", - "https://provider.meajudaai.com.br/*", - "meajudaai-provider://callback", - "http://localhost:3001/*" - ], - "webOrigins": [ - "https://localhost:7283", - "https://provider.meajudaai.com.br", - "http://localhost:3001" - ], - "frontchannelLogout": true, - "attributes": { - "pkce.code.challenge.method": "S256", - "post.logout.redirect.uris": "+" - }, - "protocolMappers": [ - { - "name": "realm-roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "access.token.claim": "true", - "id.token.claim": "true", - "userinfo.token.claim": "true", - "claim.name": "roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "clientId": "meajudaai-api-service", - "name": "MeAjudaAi API Service Account", - "description": "Service account for API to access Keycloak Admin API", - "enabled": true, - "publicClient": false, - "serviceAccountsEnabled": true, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "protocol": "openid-connect", - "attributes": { - "access.token.lifespan": "3600" - }, - "secret": "meajudaai-api-secret-dev-2024", - "authorizationServicesEnabled": false - } - ], - "users": [ - { - "username": "service-account-meajudaai-api-service", - "enabled": true, - "serviceAccountClientId": "meajudaai-api-service", - "clientRoles": { - "realm-management": [ - "view-users", - "query-users", - "view-realm" - ] - } - }, - { - "username": "admin.portal", - "enabled": true, - "firstName": "Admin", - "lastName": "Portal", - "email": "admin@meajudaai.dev", - "emailVerified": true, - "realmRoles": [ - "admin", - "meajudaai-system-admin", - "meajudaai-user-admin", - "meajudaai-provider-admin", - "meajudaai-order-admin", - "meajudaai-report-admin", - "meajudaai-catalog-manager", - "meajudaai-location-manager" - ], - "credentials": [ - { - "type": "password", - "value": "admin123", - "temporary": false - } - ] - }, - { - "username": "customer.demo", - "enabled": true, - "firstName": "Customer", - "lastName": "Demo", - "email": "customer@meajudaai.dev", - "emailVerified": true, - "realmRoles": [ - "customer" - ], - "credentials": [ - { - "type": "password", - "value": "customer123", - "temporary": false - } - ] - }, - { - "username": "provider.demo", - "enabled": true, - "firstName": "Provider", - "lastName": "Demo", - "email": "provider@meajudaai.dev", - "emailVerified": true, - "realmRoles": [ - "meajudaai-provider" - ], - "credentials": [ - { - "type": "password", - "value": "provider123", - "temporary": false - } - ] - } - ] -} \ No newline at end of file + "id": "meajudaai", + "realm": "meajudaai", + "displayName": "MeAjudaAi Development", + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": false, + "rememberMe": true, + "verifyEmail": false, + "bruteForceProtected": true, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "accessTokenLifespan": 1800, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "offlineSessionIdleTimeout": 2592000, + "loginTheme": "meajudaai", + "accountTheme": "meajudaai", + "emailTheme": "meajudaai", + "internationalizationEnabled": true, + "supportedLocales": [ + "pt-BR", + "en" + ], + "defaultLocale": "pt-BR", + "roles": { + "realm": [ + { + "name": "admin", + "description": "Administrador total da plataforma - todas as permissões" + }, + { + "name": "meajudaai-system-admin", + "description": "Administrador do sistema - todas as permissões" + }, + { + "name": "meajudaai-user-admin", + "description": "Administrador de usuários - gerenciar usuários" + }, + { + "name": "meajudaai-user-operator", + "description": "Operador de usuários - leitura e atualização" + }, + { + "name": "meajudaai-user", + "description": "Usuário básico - perfil próprio" + }, + { + "name": "meajudaai-provider-admin", + "description": "Administrador de prestadores - CRUD completo" + }, + { + "name": "meajudaai-provider", + "description": "Prestador - apenas leitura" + }, + { + "name": "meajudaai-order-admin", + "description": "Administrador de pedidos - CRUD completo" + }, + { + "name": "meajudaai-order-operator", + "description": "Operador de pedidos - leitura e atualização" + }, + { + "name": "meajudaai-report-admin", + "description": "Administrador de relatórios - criar e exportar" + }, + { + "name": "meajudaai-report-viewer", + "description": "Visualizador de relatórios - apenas visualizar" + }, + { + "name": "meajudaai-catalog-manager", + "description": "Gerente de catálogo - gerenciar serviços e categorias" + }, + { + "name": "meajudaai-location-manager", + "description": "Gerente de localidades - gerenciar cidades e estados" + }, + { + "name": "customer", + "description": "Cliente da plataforma" + } + ] + }, + "identityProviders": [ + { + "alias": "google", + "displayName": "Google", + "providerId": "google", + "enabled": true, + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "${GOOGLE_CLIENT_ID}", + "clientSecret": "${GOOGLE_CLIENT_SECRET}" + } + }, + { + "alias": "facebook", + "displayName": "Facebook", + "providerId": "facebook", + "enabled": true, + "trustEmail": true, + "storeToken": false, + "addReadTokenRoleOnCreate": false, + "authenticateByDefault": false, + "firstBrokerLoginFlowAlias": "first broker login", + "config": { + "clientId": "${FACEBOOK_APP_ID}", + "clientSecret": "${FACEBOOK_APP_SECRET}" + } + }, + { + "trustEmail": false, + "providerId": "instagram", + "enabled": true, + "authenticateByDefault": false, + "storeToken": false, + "config": { + "clientId": "${INSTAGRAM_CLIENT_ID}", + "clientSecret": "${INSTAGRAM_CLIENT_SECRET}" + }, + "alias": "instagram", + "addReadTokenRoleOnCreate": false, + "firstBrokerLoginFlowAlias": "first broker login", + "displayName": "Instagram" + } + ], + "clients": [ + { + "clientId": "admin-portal", + "name": "Admin Portal - Blazor WebAssembly", + "description": "MeAjudaAi Admin Portal OIDC Client", + "enabled": true, + "publicClient": true, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "protocol": "openid-connect", + "redirectUris": [ + "http://localhost:5165/*", + "https://localhost:7032/*", + "https://localhost:7281/*", + "https://admin.meajudaai.com.br/*" + ], + "webOrigins": [ + "http://localhost:5165", + "https://localhost:7032", + "https://localhost:7281", + "https://admin.meajudaai.com.br" + ], + "frontchannelLogout": true, + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "+" + }, + "protocolMappers": [ + { + "name": "realm-roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "clientId": "customer-app", + "name": "Customer App - Web + Mobile", + "description": "MeAjudaAi Customer App OIDC Client", + "enabled": true, + "publicClient": true, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "protocol": "openid-connect", + "redirectUris": [ + "https://localhost:7282/*", + "https://app.meajudaai.com.br/*", + "meajudaai://callback", + "http://localhost:3000/*" + ], + "webOrigins": [ + "https://localhost:7282", + "https://app.meajudaai.com.br", + "http://localhost:3000" + ], + "frontchannelLogout": true, + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "+" + }, + "protocolMappers": [ + { + "name": "realm-roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "clientId": "provider-app", + "name": "Provider App - Web + Mobile", + "description": "MeAjudaAi Provider App OIDC Client", + "enabled": true, + "publicClient": true, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "protocol": "openid-connect", + "redirectUris": [ + "https://localhost:7283/*", + "https://provider.meajudaai.com.br/*", + "meajudaai-provider://callback", + "http://localhost:3001/*" + ], + "webOrigins": [ + "https://localhost:7283", + "https://provider.meajudaai.com.br", + "http://localhost:3001" + ], + "frontchannelLogout": true, + "attributes": { + "pkce.code.challenge.method": "S256", + "post.logout.redirect.uris": "+" + }, + "protocolMappers": [ + { + "name": "realm-roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "access.token.claim": "true", + "id.token.claim": "true", + "userinfo.token.claim": "true", + "claim.name": "roles", + "jsonType.label": "String", + "multivalued": "true" + } + } + ] + }, + { + "clientId": "meajudaai-api-service", + "name": "MeAjudaAi API Service Account", + "description": "Service account for API to access Keycloak Admin API", + "enabled": true, + "publicClient": false, + "serviceAccountsEnabled": true, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "protocol": "openid-connect", + "attributes": { + "access.token.lifespan": "3600" + }, + "secret": "${CLIENT_SECRET_MEAJUDAAI_API}", + "authorizationServicesEnabled": false + } + ], + "users": [ + { + "username": "service-account-meajudaai-api-service", + "enabled": true, + "serviceAccountClientId": "meajudaai-api-service", + "clientRoles": { + "realm-management": [ + "view-users", + "query-users", + "view-realm" + ] + } + }, + { + "username": "admin.portal", + "enabled": true, + "firstName": "Admin", + "lastName": "Portal", + "email": "admin@meajudaai.dev", + "emailVerified": true, + "realmRoles": [ + "admin", + "meajudaai-system-admin", + "meajudaai-user-admin", + "meajudaai-provider-admin", + "meajudaai-order-admin", + "meajudaai-report-admin", + "meajudaai-catalog-manager", + "meajudaai-location-manager" + ], + "credentials": [ + { + "type": "password", + "value": "admin123", + "temporary": false + } + ] + }, + { + "username": "customer.demo", + "enabled": true, + "firstName": "Customer", + "lastName": "Demo", + "email": "customer@meajudaai.dev", + "emailVerified": true, + "realmRoles": [ + "customer" + ], + "credentials": [ + { + "type": "password", + "value": "customer123", + "temporary": false + } + ] + }, + { + "username": "provider.demo", + "enabled": true, + "firstName": "Provider", + "lastName": "Demo", + "email": "provider@meajudaai.dev", + "emailVerified": true, + "realmRoles": [ + "meajudaai-provider" + ], + "credentials": [ + { + "type": "password", + "value": "provider123", + "temporary": false + } + ] + } + ] +} diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj b/src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj index c9b66dfdc..212067408 100644 --- a/src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj +++ b/src/Bootstrapper/MeAjudaAi.ApiService/MeAjudaAi.ApiService.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs b/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs index 2669743d2..02f9799dc 100644 --- a/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs +++ b/src/Bootstrapper/MeAjudaAi.ApiService/Program.cs @@ -6,6 +6,7 @@ using MeAjudaAi.Modules.Documents.API; using MeAjudaAi.Modules.Locations.API; using MeAjudaAi.Modules.Providers.API; +using MeAjudaAi.Modules.Ratings.API; using MeAjudaAi.Modules.SearchProviders.API; using MeAjudaAi.Modules.ServiceCatalogs.API; using MeAjudaAi.Modules.Users.API; @@ -49,6 +50,7 @@ public static async Task Main(string[] args) builder.Services.AddLocationsModule(builder.Configuration); builder.Services.AddServiceCatalogsModule(builder.Configuration); builder.Services.AddCommunicationsModule(builder.Configuration); + builder.Services.AddRatingsModule(builder.Configuration, builder.Environment); // Shared services por último (GlobalExceptionHandler atua como fallback) builder.Services.AddSharedServices(builder.Configuration); @@ -133,6 +135,7 @@ private static async Task ConfigureMiddlewareAsync(WebApplication app) app.UseLocationsModule(); app.UseServiceCatalogsModule(); app.UseCommunicationsModule(); + app.UseRatingsModule(); // Endpoints de orquestração cross-módulo (ficam no ApiService) app.MapProviderRegistrationEndpoints(); diff --git a/src/Bootstrapper/MeAjudaAi.ApiService/packages.lock.json b/src/Bootstrapper/MeAjudaAi.ApiService/packages.lock.json index 106a5af59..dc380ba09 100644 --- a/src/Bootstrapper/MeAjudaAi.ApiService/packages.lock.json +++ b/src/Bootstrapper/MeAjudaAi.ApiService/packages.lock.json @@ -698,6 +698,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/src/Contracts/Contracts/Modules/Ratings/DTOs/CreateReviewRequest.cs b/src/Contracts/Contracts/Modules/Ratings/DTOs/CreateReviewRequest.cs new file mode 100644 index 000000000..77e855f82 --- /dev/null +++ b/src/Contracts/Contracts/Modules/Ratings/DTOs/CreateReviewRequest.cs @@ -0,0 +1,3 @@ +namespace MeAjudaAi.Contracts.Contracts.Modules.Ratings.DTOs; + +public record CreateReviewRequest(Guid ProviderId, int Rating, string? Comment); diff --git a/src/Contracts/Contracts/Modules/Ratings/DTOs/ProviderReviewResponse.cs b/src/Contracts/Contracts/Modules/Ratings/DTOs/ProviderReviewResponse.cs new file mode 100644 index 000000000..d03cbff14 --- /dev/null +++ b/src/Contracts/Contracts/Modules/Ratings/DTOs/ProviderReviewResponse.cs @@ -0,0 +1,3 @@ +namespace MeAjudaAi.Contracts.Contracts.Modules.Ratings.DTOs; + +public record ProviderReviewResponse(Guid Id, int Rating, string? Comment, DateTime CreatedAt); diff --git a/src/Contracts/Contracts/Modules/Ratings/DTOs/ReviewStatusResponse.cs b/src/Contracts/Contracts/Modules/Ratings/DTOs/ReviewStatusResponse.cs new file mode 100644 index 000000000..24e1e8e29 --- /dev/null +++ b/src/Contracts/Contracts/Modules/Ratings/DTOs/ReviewStatusResponse.cs @@ -0,0 +1,5 @@ +using MeAjudaAi.Contracts.Contracts.Modules.Ratings.Enums; + +namespace MeAjudaAi.Contracts.Contracts.Modules.Ratings.DTOs; + +public record ReviewStatusResponse(Guid Id, EReviewStatus Status); diff --git a/src/Contracts/Contracts/Modules/Ratings/Enums/EReviewStatus.cs b/src/Contracts/Contracts/Modules/Ratings/Enums/EReviewStatus.cs new file mode 100644 index 000000000..2e461f63c --- /dev/null +++ b/src/Contracts/Contracts/Modules/Ratings/Enums/EReviewStatus.cs @@ -0,0 +1,12 @@ +namespace MeAjudaAi.Contracts.Contracts.Modules.Ratings.Enums; + +/// +/// Status de uma avaliação de prestador (versão contrato). +/// +public enum EReviewStatus +{ + Pending = 0, + Approved = 1, + Rejected = 2, + Flagged = 3 +} diff --git a/src/Contracts/Identity/Enums/EAuthProvider.cs b/src/Contracts/Identity/Enums/EAuthProvider.cs index 7e5622620..c7119b64e 100644 --- a/src/Contracts/Identity/Enums/EAuthProvider.cs +++ b/src/Contracts/Identity/Enums/EAuthProvider.cs @@ -24,5 +24,10 @@ public enum EAuthProvider /// /// Login social da Apple /// - Apple + Apple, + + /// + /// Login social do Instagram + /// + Instagram } diff --git a/src/Modules/Communications/API/API.Client/CommunicationAdmin/GetCommunicationLogs.bru b/src/Modules/Communications/API/API.Client/CommunicationAdmin/GetCommunicationLogs.bru new file mode 100644 index 000000000..ddc368087 --- /dev/null +++ b/src/Modules/Communications/API/API.Client/CommunicationAdmin/GetCommunicationLogs.bru @@ -0,0 +1,44 @@ +meta { + name: Get Communication Logs + type: http + seq: 1 +} + +get { + url: {{baseUrl}}/api/v1/communications/logs?pageNumber=1&pageSize=10 + body: none + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} + +headers { + Accept: application/json +} + +docs { + # Get Communication Logs + + Retorna o histórico de comunicações (E-mail, SMS, Push) enviadas pelo sistema. + + ## Autorização + - **Requer token**: Sim (Admin) + + ## Parâmetros Query + - `pageNumber` (int): Padrão 1 + - `pageSize` (int): Padrão 10 + - `type` (string): Opcional (Email, SMS, Push) + - `status` (string): Opcional (Sent, Failed, Pending) + + ## Resposta Esperada (200 OK) + ```json + { + "items": [], + "totalCount": 0, + "pageNumber": 1, + "pageSize": 10 + } + ``` +} diff --git a/src/Modules/Communications/API/API.Client/CommunicationAdmin/GetEmailTemplates.bru b/src/Modules/Communications/API/API.Client/CommunicationAdmin/GetEmailTemplates.bru new file mode 100644 index 000000000..90f9d4b57 --- /dev/null +++ b/src/Modules/Communications/API/API.Client/CommunicationAdmin/GetEmailTemplates.bru @@ -0,0 +1,39 @@ +meta { + name: Get Email Templates + type: http + seq: 2 +} + +get { + url: {{baseUrl}}/api/v1/communications/templates + body: none + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} + +headers { + Accept: application/json +} + +docs { + # Get Email Templates + + Lista todos os templates de e-mail registrados no módulo de comunicação. + + ## Autorização + - **Requer token**: Sim (Admin) + + ## Resposta Esperada (200 OK) + ```json + [ + { + "name": "WelcomeEmail", + "subject": "Bem-vindo ao MeAjudaAi!", + "isHtml": true + } + ] + ``` +} diff --git a/src/Modules/Communications/Application/AssemblyInfo.cs b/src/Modules/Communications/Application/AssemblyInfo.cs new file mode 100644 index 000000000..020fc70a7 --- /dev/null +++ b/src/Modules/Communications/Application/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("MeAjudaAi.Modules.Communications.Tests")] +[assembly: InternalsVisibleTo("MeAjudaAi.Integration.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Modules/Communications/Application/MeAjudaAi.Modules.Communications.Application.csproj b/src/Modules/Communications/Application/MeAjudaAi.Modules.Communications.Application.csproj index 3fb799ddd..562e05d9e 100644 --- a/src/Modules/Communications/Application/MeAjudaAi.Modules.Communications.Application.csproj +++ b/src/Modules/Communications/Application/MeAjudaAi.Modules.Communications.Application.csproj @@ -6,14 +6,7 @@ enable - - - <_Parameter1>MeAjudaAi.Modules.Communications.Tests - - - <_Parameter1>MeAjudaAi.Integration.Tests - - + diff --git a/src/Modules/Communications/Application/Services/CommunicationsOutboxWorker.cs b/src/Modules/Communications/Application/Services/CommunicationsOutboxWorker.cs index 79ca395a3..0be412b18 100644 --- a/src/Modules/Communications/Application/Services/CommunicationsOutboxWorker.cs +++ b/src/Modules/Communications/Application/Services/CommunicationsOutboxWorker.cs @@ -8,22 +8,39 @@ namespace MeAjudaAi.Modules.Communications.Application.Services; /// /// Worker de background para processamento periódico do Outbox de comunicações. /// -internal sealed class CommunicationsOutboxWorker( - IServiceScopeFactory scopeFactory, - ILogger logger) : BackgroundService +internal sealed class CommunicationsOutboxWorker : BackgroundService { - private readonly TimeSpan _checkInterval = TimeSpan.FromSeconds(10); + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private readonly TimeSpan _checkInterval; + + public CommunicationsOutboxWorker( + IServiceScopeFactory scopeFactory, + ILogger logger, + TimeSpan? checkInterval = null) + { + _scopeFactory = scopeFactory; + _logger = logger; + + var interval = checkInterval ?? TimeSpan.FromSeconds(10); + if (interval <= TimeSpan.Zero && interval != Timeout.InfiniteTimeSpan) + { + throw new ArgumentOutOfRangeException(nameof(checkInterval), "Check interval must be greater than zero or InfiniteTimeSpan."); + } + + _checkInterval = interval; + } private readonly TimeSpan _stuckTimeout = TimeSpan.FromMinutes(5); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - logger.LogInformation("Communications Outbox Worker started."); + _logger.LogInformation("Communications Outbox Worker started."); while (!stoppingToken.IsCancellationRequested) { try { - using var scope = scopeFactory.CreateScope(); + using var scope = _scopeFactory.CreateScope(); // 1. Recupera mensagens travadas em processamento var repository = scope.ServiceProvider.GetRequiredService(); @@ -32,7 +49,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (resetCount > 0) { - logger.LogWarning("Reset {Count} stuck outbox messages back to Pending.", resetCount); + _logger.LogWarning("Reset {Count} stuck outbox messages back to Pending.", resetCount); } // 2. Processa mensagens pendentes @@ -41,8 +58,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (processedCount > 0) { - logger.LogInformation("Processed {Count} outbox messages.", processedCount); + _logger.LogInformation("Processed {Count} outbox messages.", processedCount); } + + await Task.Delay(_checkInterval, stoppingToken); } catch (OperationCanceledException) { @@ -50,12 +69,19 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } catch (Exception ex) { - logger.LogError(ex, "Error occurred while processing communications outbox."); + _logger.LogError(ex, "Error occurred while processing communications outbox."); + + try + { + await Task.Delay(_checkInterval, stoppingToken); + } + catch (OperationCanceledException) + { + // Exit loop + } } - - await Task.Delay(_checkInterval, stoppingToken); } - logger.LogInformation("Communications Outbox Worker stopped."); + _logger.LogInformation("Communications Outbox Worker stopped."); } } diff --git a/src/Modules/Communications/Tests/Unit/Application/ModuleApi/CommunicationsModuleApiTests.cs b/src/Modules/Communications/Tests/Unit/Application/ModuleApi/CommunicationsModuleApiTests.cs index 9c5bf2dd0..b2e49b61a 100644 --- a/src/Modules/Communications/Tests/Unit/Application/ModuleApi/CommunicationsModuleApiTests.cs +++ b/src/Modules/Communications/Tests/Unit/Application/ModuleApi/CommunicationsModuleApiTests.cs @@ -6,6 +6,7 @@ using MeAjudaAi.Modules.Communications.Domain.Repositories; using Moq; using MeAjudaAi.Contracts.Shared; +using FluentAssertions; namespace MeAjudaAi.Modules.Communications.Tests.Unit.Application.ModuleApi; @@ -113,6 +114,23 @@ public async Task SendEmailAsync_WithInvalidPriority_ShouldReturnFailure() _outboxRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); } + [Theory] + [InlineData(null, "Body")] + [InlineData("123", null)] + public async Task SendSmsAsync_WithInvalidData_ShouldReturnFailure(string? phone, string? message) + { + // Arrange + var dto = new SmsMessageDto(phone!, message!); + + // Act + var result = await _api.SendSmsAsync(dto); + + // Assert + result.IsSuccess.Should().BeFalse(); + _outboxRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + _outboxRepositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); + } + [Fact] public async Task SendSmsAsync_WithNullDto_ShouldReturnFailure() { @@ -134,17 +152,93 @@ public async Task SendPushAsync_WithNullDto_ShouldReturnFailure() } [Fact] - public async Task GetLogsAsync_WithInvalidPagination_ShouldReturnFailure() + public async Task SendSmsAsync_WithInvalidPriority_ShouldReturnFailure() { // Arrange - var query = new CommunicationLogQuery { PageNumber = 0, PageSize = 10 }; + var dto = new SmsMessageDto("123", "Body"); + + // Act + var result = await _api.SendSmsAsync(dto, (ECommunicationPriority)999); + + // Assert + result.IsSuccess.Should().BeFalse(); + _outboxRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + _outboxRepositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); + } + + [Theory] + [InlineData(null, "Title", "Body")] + [InlineData("token", null, "Body")] + [InlineData("token", "Title", null)] + public async Task SendPushAsync_WithInvalidData_ShouldReturnFailure(string? token, string? title, string? body) + { + // Arrange + var dto = new PushMessageDto(token!, title!, body!); + + // Act + var result = await _api.SendPushAsync(dto); + + // Assert + result.IsSuccess.Should().BeFalse(); + _outboxRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + _outboxRepositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task SendPushAsync_WithInvalidPriority_ShouldReturnFailure() + { + // Arrange + var dto = new PushMessageDto("token", "Title", "Body"); + + // Act + var result = await _api.SendPushAsync(dto, (ECommunicationPriority)999); + + // Assert + result.IsSuccess.Should().BeFalse(); + _outboxRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + _outboxRepositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); + } + + [Fact] + public async Task GetLogsAsync_WithNullQuery_ShouldReturnFailure() + { + // Act + var result = await _api.GetLogsAsync(null!); + + result.IsSuccess.Should().BeFalse(); + _logRepositoryMock.Verify(x => x.SearchAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); + _logRepositoryMock.VerifyNoOtherCalls(); + } + + [Theory] + [InlineData(1, 0)] + [InlineData(1, 101)] + [InlineData(0, 10)] + public async Task GetLogsAsync_WithInvalidPaginationValues_ShouldReturnFailure(int page, int size) + { + // Arrange + var query = new CommunicationLogQuery { PageNumber = page, PageSize = size }; // Act var result = await _api.GetLogsAsync(query); // Assert result.IsSuccess.Should().BeFalse(); - _logRepositoryMock.Verify(x => x.SearchAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + _logRepositoryMock.Verify(x => x.SearchAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); } [Fact] @@ -187,4 +281,14 @@ public async Task GetLogsAsync_WithValidQuery_ShouldReturnPagedResult() result.Value.Items.Should().HaveCount(1); result.Value.TotalItems.Should().Be(1); } + + [Fact] + public async Task IsAvailableAsync_ShouldReturnTrue() + { + // Act + var result = await _api.IsAvailableAsync(); + + // Assert + result.Should().BeTrue(); + } } diff --git a/src/Modules/Communications/Tests/Unit/Application/Services/CommunicationsOutboxWorkerTests.cs b/src/Modules/Communications/Tests/Unit/Application/Services/CommunicationsOutboxWorkerTests.cs new file mode 100644 index 000000000..645cd8784 --- /dev/null +++ b/src/Modules/Communications/Tests/Unit/Application/Services/CommunicationsOutboxWorkerTests.cs @@ -0,0 +1,111 @@ +using MeAjudaAi.Modules.Communications.Application.Services; +using MeAjudaAi.Modules.Communications.Domain.Repositories; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using FluentAssertions; + +namespace MeAjudaAi.Modules.Communications.Tests.Unit.Application.Services; + +public class CommunicationsOutboxWorkerTests +{ + private readonly Mock _scopeFactoryMock; + private readonly Mock _scopeMock; + private readonly Mock _serviceProviderMock; + private readonly Mock _repositoryMock; + private readonly Mock _processorMock; + private readonly Mock> _loggerMock; + + public CommunicationsOutboxWorkerTests() + { + _scopeFactoryMock = new Mock(); + _scopeMock = new Mock(); + _serviceProviderMock = new Mock(); + _repositoryMock = new Mock(); + _processorMock = new Mock(); + _loggerMock = new Mock>(); + + _scopeFactoryMock.Setup(x => x.CreateScope()).Returns(_scopeMock.Object); + _scopeMock.Setup(x => x.ServiceProvider).Returns(_serviceProviderMock.Object); + _serviceProviderMock.Setup(x => x.GetService(typeof(IOutboxMessageRepository))).Returns(_repositoryMock.Object); + _serviceProviderMock.Setup(x => x.GetService(typeof(IOutboxProcessorService))).Returns(_processorMock.Object); + } + + [Fact] + public async Task ExecuteAsync_ShouldResetStaleMessagesAndProcessPending() + { + // Arrange + var worker = new CommunicationsOutboxWorker(_scopeFactoryMock.Object, _loggerMock.Object, TimeSpan.FromMilliseconds(10)); + var cts = new CancellationTokenSource(); + var iterationTcs = new TaskCompletionSource(); + + _repositoryMock.Setup(x => x.ResetStaleProcessingMessagesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(1); + _processorMock.Setup(x => x.ProcessPendingMessagesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(1) + .Callback(() => iterationTcs.TrySetResult(true)); + + // Act + var task = worker.StartAsync(cts.Token); + + // Wait for at least one iteration to complete deterministically + await iterationTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + await worker.StopAsync(cts.Token); + + // Assert + _repositoryMock.Verify(x => x.ResetStaleProcessingMessagesAsync(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + _processorMock.Verify(x => x.ProcessPendingMessagesAsync(It.IsAny(), It.IsAny()), Times.AtLeastOnce); + } + + [Fact] + public async Task ExecuteAsync_WhenExceptionOccurs_ShouldContinue() + { + // Arrange + var worker = new CommunicationsOutboxWorker(_scopeFactoryMock.Object, _loggerMock.Object, TimeSpan.FromMilliseconds(10)); + var cts = new CancellationTokenSource(); + + var iterationCount = 0; + var secondIterationTcs = new TaskCompletionSource(); + + _repositoryMock.Setup(x => x.ResetStaleProcessingMessagesAsync(It.IsAny(), It.IsAny())) + .Returns(() => + { + iterationCount++; + if (iterationCount == 1) + { + throw new Exception("Database error"); + } + + if (iterationCount >= 2) + { + secondIterationTcs.TrySetResult(true); + } + + return Task.FromResult(0); + }); + + _processorMock.Setup(x => x.ProcessPendingMessagesAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(0); + + // Act + var task = worker.StartAsync(cts.Token); + + // Wait for the second iteration to prove it continued after the error + await secondIterationTcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + await worker.StopAsync(cts.Token); + + // Assert + _loggerMock.Verify( + x => x.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains("Error occurred")), + It.IsAny(), + It.IsAny>()), + Times.AtLeastOnce); + + iterationCount.Should().BeGreaterThanOrEqualTo(2); + } +} diff --git a/src/Modules/Communications/Tests/Unit/Application/Services/OutboxProcessorServiceTests.cs b/src/Modules/Communications/Tests/Unit/Application/Services/OutboxProcessorServiceTests.cs index df06b370a..bdf372191 100644 --- a/src/Modules/Communications/Tests/Unit/Application/Services/OutboxProcessorServiceTests.cs +++ b/src/Modules/Communications/Tests/Unit/Application/Services/OutboxProcessorServiceTests.cs @@ -7,6 +7,7 @@ using Moq; using System.Text.Json; using MeAjudaAi.Contracts.Shared; +using FluentAssertions; namespace MeAjudaAi.Modules.Communications.Tests.Unit.Application.Services; @@ -74,88 +75,103 @@ public async Task ProcessPendingMessagesAsync_WhenEmailSuccess_ShouldMarkAsSentA message.Status.Should().Be(EOutboxMessageStatus.Sent); _emailSenderMock.Verify(x => x.SendAsync(It.IsAny(), It.IsAny()), Times.Once); _logRepositoryMock.Verify(x => x.AddAsync(It.Is(l => l.IsSuccess), It.IsAny()), Times.Once); - _outboxRepositoryMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.AtLeastOnce); } [Fact] - public async Task ProcessPendingMessagesAsync_WhenSmsSuccess_ShouldMarkAsSent() + public async Task ProcessPendingMessagesAsync_WhenDispatchFails_ShouldMarkAsFailedAndRetryLater() { // Arrange - var payload = JsonSerializer.Serialize(new { PhoneNumber = "123456", Body = "Hello" }); - var message = OutboxMessage.Create(ECommunicationChannel.Sms, payload); + var payload = JsonSerializer.Serialize(new { To = "test@test.com", Subject = "Hi", Body = "Hello" }); + var message = OutboxMessage.Create(ECommunicationChannel.Email, payload, maxRetries: 3); _outboxRepositoryMock.Setup(x => x.GetPendingAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { message }); - _smsSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(true); + _emailSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(false); // Act var result = await _service.ProcessPendingMessagesAsync(); // Assert - result.Should().Be(1); - message.Status.Should().Be(EOutboxMessageStatus.Sent); - _smsSenderMock.Verify(x => x.SendAsync(It.IsAny(), It.IsAny()), Times.Once); + result.Should().Be(0); + message.Status.Should().Be(EOutboxMessageStatus.Pending); + message.RetryCount.Should().Be(1); } [Fact] - public async Task ProcessPendingMessagesAsync_WhenPushSuccess_ShouldMarkAsSent() + public async Task ProcessPendingMessagesAsync_WhenMaxRetriesReached_ShouldMarkAsFailedAndLog() { // Arrange - var payload = JsonSerializer.Serialize(new { DeviceToken = "token", Title = "Title", Body = "Body" }); - var message = OutboxMessage.Create(ECommunicationChannel.Push, payload); + var payload = JsonSerializer.Serialize(new { To = "test@test.com", Subject = "Hi", Body = "Hello" }); + var message = OutboxMessage.Create(ECommunicationChannel.Email, payload, maxRetries: 1); _outboxRepositoryMock.Setup(x => x.GetPendingAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { message }); - _pushSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(true); + _emailSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(false); // Act var result = await _service.ProcessPendingMessagesAsync(); // Assert - result.Should().Be(1); - message.Status.Should().Be(EOutboxMessageStatus.Sent); - _pushSenderMock.Verify(x => x.SendAsync(It.IsAny(), It.IsAny()), Times.Once); + result.Should().Be(0); + message.Status.Should().Be(EOutboxMessageStatus.Failed); + _logRepositoryMock.Verify(x => x.AddAsync(It.Is(l => !l.IsSuccess), It.IsAny()), Times.Once); } [Fact] - public async Task ProcessPendingMessagesAsync_WhenDispatchFails_ShouldMarkAsFailedAndRetryLater() + public async Task ProcessPendingMessagesAsync_WhenOperationCanceledDuringDispatch_ShouldReturnZeroAndStayPending() { // Arrange var payload = JsonSerializer.Serialize(new { To = "test@test.com", Subject = "Hi", Body = "Hello" }); - var message = OutboxMessage.Create(ECommunicationChannel.Email, payload, maxRetries: 3); + var message = OutboxMessage.Create(ECommunicationChannel.Email, payload); + var cts = new CancellationTokenSource(); _outboxRepositoryMock.Setup(x => x.GetPendingAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { message }); _emailSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(false); + .Returns(async (EmailMessage msg, CancellationToken ct) => + { + cts.Cancel(); // Cancel while dispatching + await Task.Yield(); + throw new OperationCanceledException(ct); + }); // Act - var result = await _service.ProcessPendingMessagesAsync(); + var result = await _service.ProcessPendingMessagesAsync(cancellationToken: cts.Token); // Assert result.Should().Be(0); message.Status.Should().Be(EOutboxMessageStatus.Pending); - message.RetryCount.Should().Be(1); - _logRepositoryMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Never); + _emailSenderMock.Verify(x => x.SendAsync(It.IsAny(), It.IsAny()), Times.Once); } [Fact] - public async Task ProcessPendingMessagesAsync_WhenDispatchThrows_ShouldHandleExceptionAndRetry() + public async Task ProcessPendingMessagesAsync_WhenUnknownChannel_ShouldFail() { // Arrange - var payload = JsonSerializer.Serialize(new { To = "test@test.com", Subject = "Hi", Body = "Hello" }); - var message = OutboxMessage.Create(ECommunicationChannel.Email, payload, maxRetries: 3); - + var message = OutboxMessage.Create((ECommunicationChannel)99, "{}"); _outboxRepositoryMock.Setup(x => x.GetPendingAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { message }); - _emailSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception("Network Error")); + // Act + var result = await _service.ProcessPendingMessagesAsync(); + + // Assert + result.Should().Be(0); + message.ErrorMessage.Should().Contain("Unknown channel"); + } + + [Fact] + public async Task ProcessPendingMessagesAsync_WhenInvalidPayload_ShouldFail() + { + // Arrange + var message = OutboxMessage.Create(ECommunicationChannel.Email, "invalid-json"); + _outboxRepositoryMock.Setup(x => x.GetPendingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List { message }); // Act var result = await _service.ProcessPendingMessagesAsync(); @@ -163,30 +179,39 @@ public async Task ProcessPendingMessagesAsync_WhenDispatchThrows_ShouldHandleExc // Assert result.Should().Be(0); message.Status.Should().Be(EOutboxMessageStatus.Pending); - message.RetryCount.Should().Be(1); - message.ErrorMessage.Should().Be("Network Error"); } [Fact] - public async Task ProcessPendingMessagesAsync_WhenMaxRetriesReached_ShouldMarkAsFailedAndLog() + public async Task ProcessPendingMessagesAsync_WithHtmlBody_ShouldUseIt() { // Arrange - var payload = JsonSerializer.Serialize(new { To = "test@test.com", Subject = "Hi", Body = "Hello" }); - var message = OutboxMessage.Create(ECommunicationChannel.Email, payload, maxRetries: 1); - + var payload = JsonSerializer.Serialize(new { To = "t@t.com", Subject = "S", HtmlBody = "

H

", TextBody = "T" }); + var message = OutboxMessage.Create(ECommunicationChannel.Email, payload); _outboxRepositoryMock.Setup(x => x.GetPendingAsync(It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(new List { message }); + _emailSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); - _emailSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(false); + // Act + await _service.ProcessPendingMessagesAsync(); + + // Assert + _emailSenderMock.Verify(x => x.SendAsync(It.Is(m => m.HtmlBody == "

H

"), It.IsAny())); + } + + [Fact] + public async Task ProcessPendingMessagesAsync_WithOnlyBody_ShouldHtmlEncodeIt() + { + // Arrange + var payload = JsonSerializer.Serialize(new { To = "t@t.com", Subject = "S", Body = "B" }); + var message = OutboxMessage.Create(ECommunicationChannel.Email, payload); + _outboxRepositoryMock.Setup(x => x.GetPendingAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(new List { message }); + _emailSenderMock.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(true); // Act - var result = await _service.ProcessPendingMessagesAsync(); + await _service.ProcessPendingMessagesAsync(); // Assert - result.Should().Be(0); - message.Status.Should().Be(EOutboxMessageStatus.Failed); - message.RetryCount.Should().Be(1); - _logRepositoryMock.Verify(x => x.AddAsync(It.Is(l => !l.IsSuccess), It.IsAny()), Times.Once); + _emailSenderMock.Verify(x => x.SendAsync(It.Is(m => m.HtmlBody.Contains("<b>")), It.IsAny())); } } diff --git a/src/Modules/Communications/Tests/packages.lock.json b/src/Modules/Communications/Tests/packages.lock.json index 49bccb0aa..9171bd5df 100644 --- a/src/Modules/Communications/Tests/packages.lock.json +++ b/src/Modules/Communications/Tests/packages.lock.json @@ -1002,6 +1002,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1159,6 +1160,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/src/Modules/Documents/API/API.Client/DocumentAdmin/GetDocumentStatus.bru b/src/Modules/Documents/API/API.Client/DocumentAdmin/GetDocumentStatus.bru new file mode 100644 index 000000000..77953bf73 --- /dev/null +++ b/src/Modules/Documents/API/API.Client/DocumentAdmin/GetDocumentStatus.bru @@ -0,0 +1,38 @@ +meta { + name: Get Document Status + type: http + seq: 4 +} + +get { + url: {{baseUrl}}/api/v1/documents/{{documentId}}/status + body: none + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} + +headers { + Accept: application/json +} + +docs { + # Get Document Status + + Consulta o status de processamento e verificação de um documento específico. + + ## Parâmetros Path + - `documentId` (guid): ID do documento + + ## Resposta Esperada (200 OK) + ```json + { + "id": "uuid", + "status": "Verified", + "rejectionReason": null, + "verifiedAt": "2026-04-13T10:00:00Z" + } + ``` +} diff --git a/src/Modules/Documents/API/API.Client/DocumentAdmin/RequestVerification.bru b/src/Modules/Documents/API/API.Client/DocumentAdmin/RequestVerification.bru new file mode 100644 index 000000000..92c4c21f1 --- /dev/null +++ b/src/Modules/Documents/API/API.Client/DocumentAdmin/RequestVerification.bru @@ -0,0 +1,30 @@ +meta { + name: Request Document Verification + type: http + seq: 5 +} + +post { + url: {{baseUrl}}/api/v1/documents/{{documentId}}/request-verification + body: none + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} + +headers { + Accept: application/json +} + +docs { + # Request Document Verification + + Solicita a verificação manual ou automática de um documento enviado. + + ## Parâmetros Path + - `documentId` (guid): ID do documento + + ## Resposta Esperada (202 Accepted) +} diff --git a/src/Modules/Documents/Tests/packages.lock.json b/src/Modules/Documents/Tests/packages.lock.json index d1f46647e..38e4be4a8 100644 --- a/src/Modules/Documents/Tests/packages.lock.json +++ b/src/Modules/Documents/Tests/packages.lock.json @@ -1014,6 +1014,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1171,6 +1172,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/src/Modules/Locations/Tests/packages.lock.json b/src/Modules/Locations/Tests/packages.lock.json index f55706a87..611524466 100644 --- a/src/Modules/Locations/Tests/packages.lock.json +++ b/src/Modules/Locations/Tests/packages.lock.json @@ -992,6 +992,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1149,6 +1150,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/src/Modules/Providers/Tests/packages.lock.json b/src/Modules/Providers/Tests/packages.lock.json index 2d96ac992..3cb68e39a 100644 --- a/src/Modules/Providers/Tests/packages.lock.json +++ b/src/Modules/Providers/Tests/packages.lock.json @@ -1023,6 +1023,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1180,6 +1181,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/src/Modules/Ratings/API/API.Client/Ratings/CreateReview.bru b/src/Modules/Ratings/API/API.Client/Ratings/CreateReview.bru new file mode 100644 index 000000000..000c02f0d --- /dev/null +++ b/src/Modules/Ratings/API/API.Client/Ratings/CreateReview.bru @@ -0,0 +1,57 @@ +meta { + name: Create Review + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/api/v1/ratings + body: json + auth: bearer +} + +auth:bearer { + token: {{accessToken}} +} + +headers { + Content-Type: application/json + Accept: application/json +} + +body:json { + { + "providerId": "00000000-0000-0000-0000-000000000000", + "rating": 5, + "comment": "Excellent service!" + } +} + +docs { + # Create Review + + Cria uma nova avaliação para um prestador de serviços. + + ## Autorização + - **Política**: Authenticated + - **Requer token**: Sim + + ## Parâmetros Body + - `providerId` (guid): ID do prestador avaliado (Obrigatório) + - `rating` (int): Nota de 1 a 5 (Obrigatório) + - `comment` (string): Comentário textual (Opcional, máx 1000 caracteres) + + ## Moderação + - Avaliações com nota >= 4 e sem comentário são auto-aprovadas. + - Comentários com palavras ofensivas são marcados para moderação manual. + + ## Resposta Esperada (201 Created) + ```json + "uuid-da-review" + ``` + + ## Códigos de Status + - **201**: Criado com sucesso + - **400**: Dados inválidos (rating fora do range, etc) + - **401**: Não autenticado +} diff --git a/src/Modules/Ratings/API/API.Client/Ratings/GetProviderReviews.bru b/src/Modules/Ratings/API/API.Client/Ratings/GetProviderReviews.bru new file mode 100644 index 000000000..64a5b9861 --- /dev/null +++ b/src/Modules/Ratings/API/API.Client/Ratings/GetProviderReviews.bru @@ -0,0 +1,41 @@ +meta { + name: Get Provider Reviews + type: http + seq: 2 +} + +get { + url: {{baseUrl}}/api/v1/ratings/provider/{{providerId}} + body: none + auth: none +} + +headers { + Accept: application/json +} + +docs { + # Get Provider Reviews + + Retorna todas as avaliações aprovadas de um prestador de serviços. + + ## Autorização + - **Pública** + - **Requer token**: Não + + ## Parâmetros Path + - `providerId` (guid): ID do prestador (Obrigatório) + + ## Resposta Esperada (200 OK) + ```json + [ + { + "id": "uuid-da-review", + "rating": 5, + "comment": "Muito bom!", + "createdAt": "2026-04-13T10:00:00Z", + "customerId": "uuid-do-cliente" + } + ] + ``` +} diff --git a/src/Modules/Ratings/API/API.Client/Ratings/GetReviewById.bru b/src/Modules/Ratings/API/API.Client/Ratings/GetReviewById.bru new file mode 100644 index 000000000..e721cb64f --- /dev/null +++ b/src/Modules/Ratings/API/API.Client/Ratings/GetReviewById.bru @@ -0,0 +1,36 @@ +meta { + name: Get Review By Id + type: http + seq: 2 +} + +get { + url: {{baseUrl}}/api/v1/ratings/{{id}} + body: none + auth: none +} + +vars:pre-request { + id: 00000000-0000-0000-0000-000000000000 +} + +docs { + # Get Review By Id + + Busca os detalhes de uma avaliação previamente postada. + + ## Autorização + - **Política**: AllowAnonymous (Público) + - **Requer token**: Não + + ## Parâmetros Route + - `id` (guid): ID da avaliação a ser buscada (Obrigatório) + + ## Comportamento + - Só retorna HTTP 200 OK caso a avaliação já esteja com status **Approved**. + - Retorna HTTP 404 Not Found caso não exista ou caso ainda esteja sob moderação / rejeitada. + + ## Códigos de Status + - **200**: Sucesso, a avaliação e dados em JSON são retornados + - **404**: Avaliação não existe ou não foi aprovada pelo comitê +} diff --git a/src/Modules/Ratings/API/API.Client/Ratings/GetReviewStatus.bru b/src/Modules/Ratings/API/API.Client/Ratings/GetReviewStatus.bru new file mode 100644 index 000000000..fc49ea705 --- /dev/null +++ b/src/Modules/Ratings/API/API.Client/Ratings/GetReviewStatus.bru @@ -0,0 +1,43 @@ +meta { + name: Get Review Status + type: http + seq: 3 +} + +get { + url: {{baseUrl}}/api/v1/ratings/{{id}}/status + body: none + auth: none +} + +vars:pre-request { + id: 00000000-0000-0000-0000-000000000000 +} + +docs { + # Get Review Status + + Retorna o status puro de uma avaliação existente, validando se está sob moderação, rejeitada ou aprovada. + + ## Autorização + - **Política**: AllowAnonymous (Público/Integrador) + - **Requer token**: Não + + ## Parâmetros Route + - `id` (guid): ID da avaliação a ser checada (Obrigatório) + + ## Comportamento + - Uma enumeração de status: 1 = Pending, 2 = Approved, 3 = Rejected. + + ## Resposta Esperada (200 OK) + ```json + { + "id": "uuid-da-review", + "status": 2 + } + ``` + + ## Códigos de Status + - **200**: Status devolvido com sucesso + - **404**: Avaliação não existe +} diff --git a/src/Modules/Ratings/API/Endpoints/RatingsEndpoints.cs b/src/Modules/Ratings/API/Endpoints/RatingsEndpoints.cs new file mode 100644 index 000000000..eb981617e --- /dev/null +++ b/src/Modules/Ratings/API/Endpoints/RatingsEndpoints.cs @@ -0,0 +1,126 @@ +using MeAjudaAi.Modules.Ratings.Application.Commands; +using MeAjudaAi.Modules.Ratings.Domain.Repositories; +using MeAjudaAi.Modules.Ratings.Domain.ValueObjects; +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Contracts.Contracts.Modules.Ratings.DTOs; +using MeAjudaAi.Contracts.Contracts.Modules.Ratings.Enums; +using MeAjudaAi.Shared.Utilities; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace MeAjudaAi.Modules.Ratings.API.Endpoints; + +public static class RatingsEndpoints +{ + public static void Map(IEndpointRouteBuilder app) + { + var group = app.MapGroup("api/v1/ratings") + .WithTags("Ratings") + .RequireAuthorization(); + + group.MapPost("/", CreateReviewAsync) + .WithName("CreateReview") + .Produces(StatusCodes.Status201Created) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status401Unauthorized); + + group.MapGet("/{id:guid}", GetReviewByIdAsync) + .WithName("GetReviewById") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound) + .AllowAnonymous(); + + group.MapGet("/{id:guid}/status", GetReviewStatusAsync) + .WithName("GetReviewStatus") + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status404NotFound); + + group.MapGet("/provider/{providerId:guid}", GetProviderReviewsAsync) + .WithName("GetProviderReviews") + .Produces>(StatusCodes.Status200OK) + .AllowAnonymous(); + } + + private static async Task CreateReviewAsync( + [FromBody] CreateReviewRequest request, + [FromServices] ICommandHandler handler, + HttpContext httpContext, + CancellationToken cancellationToken) + { + var customerId = ClaimHelpers.GetUserIdGuid(httpContext); + + if (customerId == null) + { + return Results.Unauthorized(); + } + + var command = new CreateReviewCommand( + request.ProviderId, + customerId.Value, + request.Rating, + request.Comment); + + var reviewId = await handler.HandleAsync(command, cancellationToken); + + // Location points to the status endpoint as reviews require moderation before being visible publically + return Results.Created($"/api/v1/ratings/{reviewId}/status", reviewId); + } + + private static async Task GetReviewByIdAsync( + Guid id, + [FromServices] IReviewRepository repository, + CancellationToken cancellationToken) + { + // Explicitly cast to ReviewId for clarity + var review = await repository.GetByIdAsync((ReviewId)id, cancellationToken); + + if (review == null || review.Status != MeAjudaAi.Modules.Ratings.Domain.Enums.EReviewStatus.Approved) + return Results.NotFound(); + + return Results.Ok(new ProviderReviewResponse( + review.Id.Value, + review.Rating, + review.Comment, + review.CreatedAt)); + } + + private static async Task GetReviewStatusAsync( + Guid id, + [FromServices] IReviewRepository repository, + CancellationToken cancellationToken) + { + // Explicitly cast to ReviewId for clarity + var review = await repository.GetByIdAsync((ReviewId)id, cancellationToken); + + if (review == null) + return Results.NotFound(); + + return Results.Ok(new ReviewStatusResponse( + review.Id.Value, + (EReviewStatus)(int)review.Status)); + } + + private static async Task GetProviderReviewsAsync( + Guid providerId, + [FromServices] IReviewRepository repository, + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + CancellationToken cancellationToken = default) + { + // Aplicar limites + page = page < 1 ? 1 : page; + pageSize = pageSize < 1 ? 1 : pageSize > 100 ? 100 : pageSize; + + var reviews = await repository.GetByProviderIdAsync(providerId, page, pageSize, cancellationToken); + + var result = reviews.Select(r => new ProviderReviewResponse( + r.Id.Value, + r.Rating, + r.Comment, + r.CreatedAt)); + + return Results.Ok(result); + } +} diff --git a/src/Modules/Ratings/API/Extensions.cs b/src/Modules/Ratings/API/Extensions.cs new file mode 100644 index 000000000..df618f9d4 --- /dev/null +++ b/src/Modules/Ratings/API/Extensions.cs @@ -0,0 +1,34 @@ +using MeAjudaAi.Modules.Ratings.API.Endpoints; +using MeAjudaAi.Modules.Ratings.Application; +using MeAjudaAi.Modules.Ratings.Infrastructure; +using MeAjudaAi.Modules.Ratings.Infrastructure.Events.Handlers; +using MeAjudaAi.Modules.Ratings.Domain.Events; +using MeAjudaAi.Shared.Events; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace MeAjudaAi.Modules.Ratings.API; + +public static class Extensions +{ + public static IServiceCollection AddRatingsModule(this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment) + { + services.AddApplication(); + services.AddInfrastructure(configuration, environment); + + // Manipuladores de eventos de domínio + services.AddScoped, ReviewApprovedDomainEventHandler>(); + services.AddScoped, ReviewRejectedDomainEventHandler>(); + + return services; + } + + public static IEndpointRouteBuilder UseRatingsModule(this IEndpointRouteBuilder app) + { + RatingsEndpoints.Map(app); + return app; + } +} diff --git a/src/Modules/Ratings/API/MeAjudaAi.Modules.Ratings.API.csproj b/src/Modules/Ratings/API/MeAjudaAi.Modules.Ratings.API.csproj new file mode 100644 index 000000000..1fbea747a --- /dev/null +++ b/src/Modules/Ratings/API/MeAjudaAi.Modules.Ratings.API.csproj @@ -0,0 +1,13 @@ + + + + net10.0 + enable + enable + + + + + + + diff --git a/src/Modules/Ratings/API/packages.lock.json b/src/Modules/Ratings/API/packages.lock.json new file mode 100644 index 000000000..8b6476a5e --- /dev/null +++ b/src/Modules/Ratings/API/packages.lock.json @@ -0,0 +1,844 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "SonarAnalyzer.CSharp": { + "type": "Direct", + "requested": "[10.23.0.137933, )", + "resolved": "10.23.0.137933", + "contentHash": "K8CwUkB4PlUcMfLqGCj1aviVfW9AufZH2K/TjgPGYdj4gosV/kcyxpBR0jaCG8argC6dfxJBbSS6xXJ59lFWgA==" + }, + "Asp.Versioning.Abstractions": { + "type": "Transitive", + "resolved": "8.1.0", + "contentHash": "mpeNZyMdvrHztJwR1sXIUQ+3iioEU97YMBnFA9WLbsPOYhGwDJnqJMmEd8ny7kcmS9OjTHoEuX/bSXXY3brIFA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Dapper.AOT": { + "type": "Transitive", + "resolved": "1.0.48", + "contentHash": "rsLM3yKr4g+YKKox9lhc8D+kz67P7Q9+xdyn1LmCsoYr1kYpJSm+Nt6slo5UrfUrcTiGJ57zUlyO8XUdV7G7iA==" + }, + "Hangfire.NetCore": { + "type": "Transitive", + "resolved": "1.8.23", + "contentHash": "SmvUJF/u5MCP666R5Y1V+GntqBc4RCWJqn5ztMMN67d53Cx5cuaWR0YNLMrabjylwLarFYJ7EdR9RnGEZzp/dg==", + "dependencies": { + "Hangfire.Core": "[1.8.23]", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "3.0.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Bcl.TimeProvider": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.11.31", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Microsoft.VisualStudio.SolutionPersistence": "1.0.52", + "Newtonsoft.Json": "13.0.3", + "System.Composition": "9.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "zLgN22Zp9pk8RHlwssRTexw4+a6wqOnKWN+VejdPn5Yhjql4XiBhkFo35Nu8mmqHIk/UEmmCnMGLWq75aFfkOw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.11", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.1", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "So3JUdRxozRjvQ3cxU6F3nI/i4emDnjane6yMYcJhvTTTu29ltlIdoXjkFGRceIWz8yKvuEpzXItZ0x5GvN2nQ==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.FeatureManagement": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "qxvGAv9WJHYfOpixWywJTa1WNTPy5MbQiv+O+UlE6E/LVofiM1+YRR6m41zsHIbAGm1S0PQ0QFuAsOw9DkoKsg==", + "dependencies": { + "Microsoft.Bcl.TimeProvider": "8.0.1", + "Microsoft.Extensions.Caching.Memory": "8.0.1", + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.2", + "Microsoft.Extensions.Logging": "8.0.1" + } + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "Pipelines.Sockets.Unofficial": { + "type": "Transitive", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "StackExchange.Redis": { + "type": "Transitive", + "resolved": "2.7.27", + "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "meajudaai.contracts": { + "type": "Project", + "dependencies": { + "FluentValidation": "[12.1.1, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, + "meajudaai.shared": { + "type": "Project", + "dependencies": { + "Asp.Versioning.Mvc": "[8.1.1, )", + "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )", + "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )", + "AspNetCore.HealthChecks.Redis": "[9.0.0, )", + "Dapper": "[2.1.72, )", + "EFCore.NamingConventions": "[10.0.1, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Hangfire.AspNetCore": "[1.8.23, )", + "Hangfire.Core": "[1.8.23, )", + "Hangfire.PostgreSql": "[1.21.1, )", + "MeAjudaAi.Contracts": "[1.0.0, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )", + "Microsoft.FeatureManagement.AspNetCore": "[4.4.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "RabbitMQ.Client": "[7.2.1, )", + "Rebus": "[8.9.0, )", + "Rebus.RabbitMq": "[10.1.1, )", + "Rebus.ServiceProvider": "[10.7.2, )", + "Scrutor": "[7.0.0, )", + "Serilog": "[4.3.1, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Enrichers.Environment": "[3.0.1, )", + "Serilog.Enrichers.Process": "[3.0.0, )", + "Serilog.Enrichers.Thread": "[4.0.0, )", + "Serilog.Settings.Configuration": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.Seq": "[9.0.0, )" + } + }, + "Asp.Versioning.Http": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "1D/Mzq1MSUSQe1eCY6GeEsu+tIlpzoWZxZkhlcw/uvtVoTrwO+BMl0fSj/XX8oZN1DutWTZqHDHgyDRq+IeSdQ==", + "dependencies": { + "Asp.Versioning.Abstractions": "8.1.0" + } + }, + "Asp.Versioning.Mvc": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "mkJv6eAdlbHbqTdrUcfEYFoZuGL6HoR7O+Lfsvivixp7N5BNhfCFPPOwsBzdIiH1qzdJXyJf+C+DZ08j27PMPg==", + "dependencies": { + "Asp.Versioning.Http": "8.1.1" + } + }, + "Asp.Versioning.Mvc.ApiExplorer": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "u8PrP6CjmurgIh0EDfg8Gc/GdpDHgkbv8OLKdxQcWb5W4sp0i9BcdbQlLvRcATjNIJUyr7ZmqXjmdsFfqnuy0g==", + "dependencies": { + "Asp.Versioning.Mvc": "8.1.1" + } + }, + "AspNetCore.HealthChecks.NpgSql": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "Npgsql": "8.0.3" + } + }, + "AspNetCore.HealthChecks.Redis": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "StackExchange.Redis": "2.7.4" + } + }, + "Dapper": { + "type": "CentralTransitive", + "requested": "[2.1.72, )", + "resolved": "2.1.72", + "contentHash": "ns4mGqQd9a/MhP8m6w556vVlZIa0/MfUu03zrxjZC/jlr1uVCsUac8bkdB+Fs98Llbd56rRSo1eZH5VVmeGZyw==" + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0" + } + }, + "Hangfire.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "TXpOl7kX4xXq5bLEqqWCpt9zh3TaouDwtb3GDtzGHX5uSC2RaAqZzn2swevivx3Uki16slXIigiPtgr4TPKpsg==", + "dependencies": { + "Hangfire.NetCore": "[1.8.23]" + } + }, + "Hangfire.Core": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "YCOTtF3NNOQI83PlfjeNDDBkofJDfdET2CwhfQsiVBwmsU6lP19QW9NVTIH9epl+MnOsyFC2G1RnlPSGV8F1FQ==", + "dependencies": { + "Newtonsoft.Json": "11.0.1" + } + }, + "Hangfire.PostgreSql": { + "type": "CentralTransitive", + "requested": "[1.21.1, )", + "resolved": "1.21.1", + "contentHash": "hFNZAxv+1p72/XCZdImnH6ovCzZ2DKAMTOI8CReT0P3yw/k0b0YJP2teA18agNH1ZYInPzhtxGk8hx5n2cxbbQ==", + "dependencies": { + "Dapper": "2.0.123", + "Dapper.AOT": "1.0.48", + "Hangfire.Core": "1.8.0", + "Npgsql": "6.0.11" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.Build.Framework": { + "type": "CentralTransitive", + "requested": "[18.0.2, )", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Hybrid": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.4", + "Microsoft.Extensions.Caching.Memory": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.StackExchangeRedis": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "StackExchange.Redis": "2.7.27" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Options": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.FeatureManagement.AspNetCore": { + "type": "CentralTransitive", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "jE5KxeEDUnUsx1w9uOFV5cOfM4E2mg7kf07328xO1x4JdoS0jwkD55nMjTlUPu90ynYX1oCBGC+FHK6ZoqRpxA==", + "dependencies": { + "Microsoft.FeatureManagement": "4.4.0" + } + }, + "Microsoft.OpenApi": { + "type": "CentralTransitive", + "requested": "[2.7.0, )", + "resolved": "2.7.0", + "contentHash": "b9xmpnmjq6p+HqF3uWG7u7/PlB38t/UB5UtXdi6xEAP9ZJGKHneYyjMGzBflB1rpLxYEcU6KRme+cz5wNPlxqA==" + }, + "Npgsql": { + "type": "CentralTransitive", + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Rebus": { + "type": "CentralTransitive", + "requested": "[8.9.0, )", + "resolved": "8.9.0", + "contentHash": "UaPGZuXIL4J5GUDA05JzEEzuPMEXY0CoF92nC6bsFBPvwoYPQ0uKyH2vKqdV80CW7cjbwBgDlEZ7R9hO9b59XA==", + "dependencies": { + "Newtonsoft.Json": "13.0.4" + } + }, + "Rebus.RabbitMq": { + "type": "CentralTransitive", + "requested": "[10.1.1, )", + "resolved": "10.1.1", + "contentHash": "66pUp4hfaYWfQEDOiVcuZQnPF4XFHyJ5KCfwCm18e3Dnr936Iog48KrN8Mp8QyRQ2tiNpzdjSATQLKEZpSk11A==", + "dependencies": { + "RabbitMq.Client": "7.1.2", + "rebus": "8.9.0" + } + }, + "Rebus.ServiceProvider": { + "type": "CentralTransitive", + "requested": "[10.7.2, )", + "resolved": "10.7.2", + "contentHash": "Qa8sKt1i9Fy/zCw5GwAUsfT+lt4BvkIgYh8sRJ6fvqJWoedS//pfcyiKUUb0wL3C5Wrpi3U+vRud5DCbMHaFIw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[8.0.0, 11.0.0)", + "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, 11.0.0)", + "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, 11.0.0)", + "Rebus": "8.9.0" + } + }, + "Scrutor": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "wHWaroody48jnlLoq/REwUltIFLxplXyHTP+sttrc8P7+jkiVqf38afDidJNv4qgD/6zz2NKOYg06xLZ1bG7wQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0" + } + }, + "Serilog": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Enrichers.Environment": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "9BqCE4C9FF+/rJb/CsQwe7oVf44xqkOvMwX//CUxvUR25lFL4tSS6iuxE5eW07quby1BAyAEP+vM6TWsnT3iqw==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Process": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "/wPYz2PDCJGSHNI+Z0PAacZvrgZgrGduWqLXeC2wvW6pgGM/Bi45JrKy887MRcRPHIZVU0LAlkmJ7TkByC0boQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Thread": { + "type": "CentralTransitive", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "C7BK25a1rhUyr+Tp+1BYcVlBJq7M2VCHlIgnwoIUVJcicM9jYcvQK18+OeHiXw7uLPSjqWxJIp1EfaZ/RGmEwA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Seq": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==", + "dependencies": { + "Serilog": "4.2.0", + "Serilog.Sinks.File": "6.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Ratings/Application/Commands/CreateReviewCommand.cs b/src/Modules/Ratings/Application/Commands/CreateReviewCommand.cs new file mode 100644 index 000000000..9a7a83e8a --- /dev/null +++ b/src/Modules/Ratings/Application/Commands/CreateReviewCommand.cs @@ -0,0 +1,14 @@ +using MeAjudaAi.Shared.Commands; +using MeAjudaAi.Shared.Utilities; + +namespace MeAjudaAi.Modules.Ratings.Application.Commands; + +public record CreateReviewCommand( + Guid ProviderId, + Guid CustomerId, + int Rating, + string? Comment +) : ICommand +{ + public Guid CorrelationId { get; init; } = UuidGenerator.NewId(); +} diff --git a/src/Modules/Ratings/Application/Extensions.cs b/src/Modules/Ratings/Application/Extensions.cs new file mode 100644 index 000000000..4dd7a33ce --- /dev/null +++ b/src/Modules/Ratings/Application/Extensions.cs @@ -0,0 +1,19 @@ +using MeAjudaAi.Modules.Ratings.Application.Commands; +using MeAjudaAi.Modules.Ratings.Application.Handlers; +using MeAjudaAi.Modules.Ratings.Application.Services; +using MeAjudaAi.Shared.Commands; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace MeAjudaAi.Modules.Ratings.Application; + +public static class Extensions +{ + public static IServiceCollection AddApplication(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped, CreateReviewCommandHandler>(); + + return services; + } +} diff --git a/src/Modules/Ratings/Application/Handlers/CreateReviewCommandHandler.cs b/src/Modules/Ratings/Application/Handlers/CreateReviewCommandHandler.cs new file mode 100644 index 000000000..99a95d809 --- /dev/null +++ b/src/Modules/Ratings/Application/Handlers/CreateReviewCommandHandler.cs @@ -0,0 +1,70 @@ +using MeAjudaAi.Modules.Ratings.Application.Commands; +using MeAjudaAi.Modules.Ratings.Application.Services; +using MeAjudaAi.Modules.Ratings.Domain.Entities; +using MeAjudaAi.Modules.Ratings.Domain.Exceptions; +using MeAjudaAi.Modules.Ratings.Domain.Repositories; +using MeAjudaAi.Shared.Commands; +using Microsoft.Extensions.Logging; + +namespace MeAjudaAi.Modules.Ratings.Application.Handlers; + +public sealed class CreateReviewCommandHandler( + IReviewRepository repository, + IContentModerator contentModerator, + ILogger logger) : ICommandHandler +{ + public async Task HandleAsync(CreateReviewCommand command, CancellationToken cancellationToken = default) + { + logger.LogInformation("Creating review for provider {ProviderId} by customer {CustomerId}", command.ProviderId, command.CustomerId); + + // Validar duplicidade (UX check antecipado) + var existingReview = await repository.GetByProviderAndCustomerAsync(command.ProviderId, command.CustomerId, cancellationToken); + if (existingReview != null) + { + logger.LogWarning("Customer {CustomerId} already reviewed provider {ProviderId}", command.CustomerId, command.ProviderId); + throw new InvalidOperationException("Você já avaliou este prestador."); + } + + var review = Review.Create( + command.ProviderId, + command.CustomerId, + command.Rating, + command.Comment); + + // Moderação Automática e Regras de Auto-aprovação + // Short-circuit: Se não houver comentário, aplicar regra de auto-aprovação direta + if (string.IsNullOrWhiteSpace(command.Comment)) + { + if (command.Rating >= 4) + { + review.Approve(); + } + } + else + { + // Se houver comentário, passar pela moderação + var isClean = contentModerator.IsClean(command.Comment); + if (!isClean) + { + logger.LogWarning("Review {ReviewId} flagged for moderation due to inappropriate content", review.Id.Value); + review.MarkAsFlagged(); + } + // Se isClean for true, o status permanece Pending por padrão para moderação manual + } + + try + { + await repository.AddAsync(review, cancellationToken); + } + catch (DuplicateReviewException ex) + { + logger.LogWarning(ex, "Duplicate review detected at persistence level for Provider {ProviderId} and Customer {CustomerId}", + command.ProviderId, command.CustomerId); + throw new InvalidOperationException("Você já avaliou este prestador."); + } + + logger.LogInformation("Review {ReviewId} created with status {Status}", review.Id.Value, review.Status); + + return review.Id.Value; + } +} diff --git a/src/Modules/Ratings/Application/MeAjudaAi.Modules.Ratings.Application.csproj b/src/Modules/Ratings/Application/MeAjudaAi.Modules.Ratings.Application.csproj new file mode 100644 index 000000000..dcd614aeb --- /dev/null +++ b/src/Modules/Ratings/Application/MeAjudaAi.Modules.Ratings.Application.csproj @@ -0,0 +1,27 @@ + + + + net10.0 + enable + enable + + + + + <_Parameter1>MeAjudaAi.Modules.Ratings.Tests + + + <_Parameter1>MeAjudaAi.Modules.Ratings.Infrastructure + + + + + + + + + + + + + diff --git a/src/Modules/Ratings/Application/Services/ContentModerator.cs b/src/Modules/Ratings/Application/Services/ContentModerator.cs new file mode 100644 index 000000000..95ba9e7cf --- /dev/null +++ b/src/Modules/Ratings/Application/Services/ContentModerator.cs @@ -0,0 +1,23 @@ +using System.Text.RegularExpressions; + +namespace MeAjudaAi.Modules.Ratings.Application.Services; + +public interface IContentModerator +{ + bool IsClean(string? content); +} + +public class ContentModerator : IContentModerator +{ + private static readonly string[] BadWords = ["idiota", "burro", "lixo", "golpe", "fake"]; + private static readonly List Patterns = BadWords + .Select(word => new Regex($@"\b{Regex.Escape(word)}\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled)) + .ToList(); + + public bool IsClean(string? content) + { + if (string.IsNullOrWhiteSpace(content)) return true; + + return !Patterns.Any(regex => regex.IsMatch(content)); + } +} diff --git a/src/Modules/Ratings/Application/packages.lock.json b/src/Modules/Ratings/Application/packages.lock.json new file mode 100644 index 000000000..960653ac9 --- /dev/null +++ b/src/Modules/Ratings/Application/packages.lock.json @@ -0,0 +1,827 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Npgsql": { + "type": "Direct", + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "SonarAnalyzer.CSharp": { + "type": "Direct", + "requested": "[10.23.0.137933, )", + "resolved": "10.23.0.137933", + "contentHash": "K8CwUkB4PlUcMfLqGCj1aviVfW9AufZH2K/TjgPGYdj4gosV/kcyxpBR0jaCG8argC6dfxJBbSS6xXJ59lFWgA==" + }, + "Asp.Versioning.Abstractions": { + "type": "Transitive", + "resolved": "8.1.0", + "contentHash": "mpeNZyMdvrHztJwR1sXIUQ+3iioEU97YMBnFA9WLbsPOYhGwDJnqJMmEd8ny7kcmS9OjTHoEuX/bSXXY3brIFA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Dapper.AOT": { + "type": "Transitive", + "resolved": "1.0.48", + "contentHash": "rsLM3yKr4g+YKKox9lhc8D+kz67P7Q9+xdyn1LmCsoYr1kYpJSm+Nt6slo5UrfUrcTiGJ57zUlyO8XUdV7G7iA==" + }, + "Hangfire.NetCore": { + "type": "Transitive", + "resolved": "1.8.23", + "contentHash": "SmvUJF/u5MCP666R5Y1V+GntqBc4RCWJqn5ztMMN67d53Cx5cuaWR0YNLMrabjylwLarFYJ7EdR9RnGEZzp/dg==", + "dependencies": { + "Hangfire.Core": "[1.8.23]", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "3.0.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Bcl.TimeProvider": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.11.31", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Microsoft.VisualStudio.SolutionPersistence": "1.0.52", + "Newtonsoft.Json": "13.0.3", + "System.Composition": "9.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "zLgN22Zp9pk8RHlwssRTexw4+a6wqOnKWN+VejdPn5Yhjql4XiBhkFo35Nu8mmqHIk/UEmmCnMGLWq75aFfkOw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.11", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.1", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "So3JUdRxozRjvQ3cxU6F3nI/i4emDnjane6yMYcJhvTTTu29ltlIdoXjkFGRceIWz8yKvuEpzXItZ0x5GvN2nQ==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.FeatureManagement": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "qxvGAv9WJHYfOpixWywJTa1WNTPy5MbQiv+O+UlE6E/LVofiM1+YRR6m41zsHIbAGm1S0PQ0QFuAsOw9DkoKsg==", + "dependencies": { + "Microsoft.Bcl.TimeProvider": "8.0.1", + "Microsoft.Extensions.Caching.Memory": "8.0.1", + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.2", + "Microsoft.Extensions.Logging": "8.0.1" + } + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "Pipelines.Sockets.Unofficial": { + "type": "Transitive", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "StackExchange.Redis": { + "type": "Transitive", + "resolved": "2.7.27", + "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "meajudaai.contracts": { + "type": "Project", + "dependencies": { + "FluentValidation": "[12.1.1, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.shared": { + "type": "Project", + "dependencies": { + "Asp.Versioning.Mvc": "[8.1.1, )", + "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )", + "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )", + "AspNetCore.HealthChecks.Redis": "[9.0.0, )", + "Dapper": "[2.1.72, )", + "EFCore.NamingConventions": "[10.0.1, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Hangfire.AspNetCore": "[1.8.23, )", + "Hangfire.Core": "[1.8.23, )", + "Hangfire.PostgreSql": "[1.21.1, )", + "MeAjudaAi.Contracts": "[1.0.0, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )", + "Microsoft.FeatureManagement.AspNetCore": "[4.4.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "RabbitMQ.Client": "[7.2.1, )", + "Rebus": "[8.9.0, )", + "Rebus.RabbitMq": "[10.1.1, )", + "Rebus.ServiceProvider": "[10.7.2, )", + "Scrutor": "[7.0.0, )", + "Serilog": "[4.3.1, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Enrichers.Environment": "[3.0.1, )", + "Serilog.Enrichers.Process": "[3.0.0, )", + "Serilog.Enrichers.Thread": "[4.0.0, )", + "Serilog.Settings.Configuration": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.Seq": "[9.0.0, )" + } + }, + "Asp.Versioning.Http": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "1D/Mzq1MSUSQe1eCY6GeEsu+tIlpzoWZxZkhlcw/uvtVoTrwO+BMl0fSj/XX8oZN1DutWTZqHDHgyDRq+IeSdQ==", + "dependencies": { + "Asp.Versioning.Abstractions": "8.1.0" + } + }, + "Asp.Versioning.Mvc": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "mkJv6eAdlbHbqTdrUcfEYFoZuGL6HoR7O+Lfsvivixp7N5BNhfCFPPOwsBzdIiH1qzdJXyJf+C+DZ08j27PMPg==", + "dependencies": { + "Asp.Versioning.Http": "8.1.1" + } + }, + "Asp.Versioning.Mvc.ApiExplorer": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "u8PrP6CjmurgIh0EDfg8Gc/GdpDHgkbv8OLKdxQcWb5W4sp0i9BcdbQlLvRcATjNIJUyr7ZmqXjmdsFfqnuy0g==", + "dependencies": { + "Asp.Versioning.Mvc": "8.1.1" + } + }, + "AspNetCore.HealthChecks.NpgSql": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "Npgsql": "8.0.3" + } + }, + "AspNetCore.HealthChecks.Redis": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "StackExchange.Redis": "2.7.4" + } + }, + "Dapper": { + "type": "CentralTransitive", + "requested": "[2.1.72, )", + "resolved": "2.1.72", + "contentHash": "ns4mGqQd9a/MhP8m6w556vVlZIa0/MfUu03zrxjZC/jlr1uVCsUac8bkdB+Fs98Llbd56rRSo1eZH5VVmeGZyw==" + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0" + } + }, + "Hangfire.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "TXpOl7kX4xXq5bLEqqWCpt9zh3TaouDwtb3GDtzGHX5uSC2RaAqZzn2swevivx3Uki16slXIigiPtgr4TPKpsg==", + "dependencies": { + "Hangfire.NetCore": "[1.8.23]" + } + }, + "Hangfire.Core": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "YCOTtF3NNOQI83PlfjeNDDBkofJDfdET2CwhfQsiVBwmsU6lP19QW9NVTIH9epl+MnOsyFC2G1RnlPSGV8F1FQ==", + "dependencies": { + "Newtonsoft.Json": "11.0.1" + } + }, + "Hangfire.PostgreSql": { + "type": "CentralTransitive", + "requested": "[1.21.1, )", + "resolved": "1.21.1", + "contentHash": "hFNZAxv+1p72/XCZdImnH6ovCzZ2DKAMTOI8CReT0P3yw/k0b0YJP2teA18agNH1ZYInPzhtxGk8hx5n2cxbbQ==", + "dependencies": { + "Dapper": "2.0.123", + "Dapper.AOT": "1.0.48", + "Hangfire.Core": "1.8.0", + "Npgsql": "6.0.11" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.Build.Framework": { + "type": "CentralTransitive", + "requested": "[18.0.2, )", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Hybrid": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.4", + "Microsoft.Extensions.Caching.Memory": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.StackExchangeRedis": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "StackExchange.Redis": "2.7.27" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Options": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.FeatureManagement.AspNetCore": { + "type": "CentralTransitive", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "jE5KxeEDUnUsx1w9uOFV5cOfM4E2mg7kf07328xO1x4JdoS0jwkD55nMjTlUPu90ynYX1oCBGC+FHK6ZoqRpxA==", + "dependencies": { + "Microsoft.FeatureManagement": "4.4.0" + } + }, + "Microsoft.OpenApi": { + "type": "CentralTransitive", + "requested": "[2.7.0, )", + "resolved": "2.7.0", + "contentHash": "b9xmpnmjq6p+HqF3uWG7u7/PlB38t/UB5UtXdi6xEAP9ZJGKHneYyjMGzBflB1rpLxYEcU6KRme+cz5wNPlxqA==" + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Rebus": { + "type": "CentralTransitive", + "requested": "[8.9.0, )", + "resolved": "8.9.0", + "contentHash": "UaPGZuXIL4J5GUDA05JzEEzuPMEXY0CoF92nC6bsFBPvwoYPQ0uKyH2vKqdV80CW7cjbwBgDlEZ7R9hO9b59XA==", + "dependencies": { + "Newtonsoft.Json": "13.0.4" + } + }, + "Rebus.RabbitMq": { + "type": "CentralTransitive", + "requested": "[10.1.1, )", + "resolved": "10.1.1", + "contentHash": "66pUp4hfaYWfQEDOiVcuZQnPF4XFHyJ5KCfwCm18e3Dnr936Iog48KrN8Mp8QyRQ2tiNpzdjSATQLKEZpSk11A==", + "dependencies": { + "RabbitMq.Client": "7.1.2", + "rebus": "8.9.0" + } + }, + "Rebus.ServiceProvider": { + "type": "CentralTransitive", + "requested": "[10.7.2, )", + "resolved": "10.7.2", + "contentHash": "Qa8sKt1i9Fy/zCw5GwAUsfT+lt4BvkIgYh8sRJ6fvqJWoedS//pfcyiKUUb0wL3C5Wrpi3U+vRud5DCbMHaFIw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[8.0.0, 11.0.0)", + "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, 11.0.0)", + "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, 11.0.0)", + "Rebus": "8.9.0" + } + }, + "Scrutor": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "wHWaroody48jnlLoq/REwUltIFLxplXyHTP+sttrc8P7+jkiVqf38afDidJNv4qgD/6zz2NKOYg06xLZ1bG7wQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0" + } + }, + "Serilog": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Enrichers.Environment": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "9BqCE4C9FF+/rJb/CsQwe7oVf44xqkOvMwX//CUxvUR25lFL4tSS6iuxE5eW07quby1BAyAEP+vM6TWsnT3iqw==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Process": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "/wPYz2PDCJGSHNI+Z0PAacZvrgZgrGduWqLXeC2wvW6pgGM/Bi45JrKy887MRcRPHIZVU0LAlkmJ7TkByC0boQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Thread": { + "type": "CentralTransitive", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "C7BK25a1rhUyr+Tp+1BYcVlBJq7M2VCHlIgnwoIUVJcicM9jYcvQK18+OeHiXw7uLPSjqWxJIp1EfaZ/RGmEwA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Seq": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==", + "dependencies": { + "Serilog": "4.2.0", + "Serilog.Sinks.File": "6.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Ratings/Domain/Entities/Review.cs b/src/Modules/Ratings/Domain/Entities/Review.cs new file mode 100644 index 000000000..90fcfe772 --- /dev/null +++ b/src/Modules/Ratings/Domain/Entities/Review.cs @@ -0,0 +1,118 @@ +using MeAjudaAi.Modules.Ratings.Domain.Enums; +using MeAjudaAi.Modules.Ratings.Domain.Events; +using MeAjudaAi.Modules.Ratings.Domain.ValueObjects; +using MeAjudaAi.Shared.Domain; + +namespace MeAjudaAi.Modules.Ratings.Domain.Entities; + +/// +/// Representa uma avaliação de um prestador de serviços feita por um cliente. +/// +public sealed class Review : AggregateRoot +{ + /// + /// ID do prestador avaliado. + /// + public Guid ProviderId { get; private set; } + + /// + /// ID do cliente que avaliou. + /// + public Guid CustomerId { get; private set; } + + /// + /// Nota da avaliação (1 a 5 estrelas). + /// + public int Rating { get; private set; } + + /// + /// Comentário textual da avaliação. + /// + public string? Comment { get; private set; } + + /// + /// Status atual da avaliação para moderação. + /// + public EReviewStatus Status { get; private set; } + + /// + /// Motivo de rejeição (se aplicável). + /// + public string? RejectionReason { get; private set; } + + private Review() { } + + private Review(ReviewId id, Guid providerId, Guid customerId, int rating, string? comment) : base(id) + { + ProviderId = providerId; + CustomerId = customerId; + Rating = rating; + Comment = comment; + Status = EReviewStatus.Pending; + + AddDomainEvent(new ReviewCreatedDomainEvent( + Id, + 0, + ProviderId, + CustomerId, + Rating, + Comment)); + } + + /// + /// Cria uma nova avaliação. + /// + public static Review Create(Guid providerId, Guid customerId, int rating, string? comment) + { + if (providerId == Guid.Empty) throw new ArgumentException("ProviderId não pode ser vazio", nameof(providerId)); + if (customerId == Guid.Empty) throw new ArgumentException("CustomerId não pode ser vazio", nameof(customerId)); + if (rating < 1 || rating > 5) throw new ArgumentOutOfRangeException(nameof(rating), "Rating deve ser entre 1 e 5"); + + return new Review(ReviewId.New(), providerId, customerId, rating, comment); + } + + /// + /// Aprova a avaliação tornando-a pública. + /// + public void Approve() + { + if (Status == EReviewStatus.Approved) return; + + Status = EReviewStatus.Approved; + RejectionReason = null; // Limpa o motivo de rejeição se estava rejeitado anteriormente + MarkAsUpdated(); + + AddDomainEvent(new ReviewApprovedDomainEvent( + Id, + 0, // Versionamento simples por enquanto + ProviderId, + Rating, + Comment)); + } + + /// + /// Rejeita a avaliação por violação de regras. + /// + public void Reject(string reason) + { + if (string.IsNullOrWhiteSpace(reason)) throw new ArgumentException("Motivo de rejeição é obrigatório", nameof(reason)); + if (Status == EReviewStatus.Rejected) return; + + Status = EReviewStatus.Rejected; + RejectionReason = reason; + MarkAsUpdated(); + + AddDomainEvent(new ReviewRejectedDomainEvent(Id, 0, ProviderId, reason)); + } + + /// + /// Sinaliza a avaliação para moderação manual. + /// + public void MarkAsFlagged() + { + if (Status == EReviewStatus.Flagged) return; + + Status = EReviewStatus.Flagged; + MarkAsUpdated(); + } +} diff --git a/src/Modules/Ratings/Domain/Enums/EReviewStatus.cs b/src/Modules/Ratings/Domain/Enums/EReviewStatus.cs new file mode 100644 index 000000000..2af4fea0d --- /dev/null +++ b/src/Modules/Ratings/Domain/Enums/EReviewStatus.cs @@ -0,0 +1,27 @@ +namespace MeAjudaAi.Modules.Ratings.Domain.Enums; + +/// +/// Status de uma avaliação de prestador. +/// +public enum EReviewStatus +{ + /// + /// Avaliação aguardando moderação. + /// + Pending = 0, + + /// + /// Avaliação aprovada e visível publicamente. + /// + Approved = 1, + + /// + /// Avaliação rejeitada por violação de termos. + /// + Rejected = 2, + + /// + /// Avaliação sinalizada para revisão manual. + /// + Flagged = 3 +} diff --git a/src/Modules/Ratings/Domain/Events/ReviewApprovedDomainEvent.cs b/src/Modules/Ratings/Domain/Events/ReviewApprovedDomainEvent.cs new file mode 100644 index 000000000..0cdac552a --- /dev/null +++ b/src/Modules/Ratings/Domain/Events/ReviewApprovedDomainEvent.cs @@ -0,0 +1,14 @@ +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Ratings.Domain.Events; + +/// +/// Evento de domínio disparado quando uma avaliação é aprovada. +/// +public record ReviewApprovedDomainEvent( + Guid AggregateId, + int Version, + Guid ProviderId, + int Rating, + string? Comment +) : DomainEvent(AggregateId, Version); diff --git a/src/Modules/Ratings/Domain/Events/ReviewCreatedDomainEvent.cs b/src/Modules/Ratings/Domain/Events/ReviewCreatedDomainEvent.cs new file mode 100644 index 000000000..e6dc084a6 --- /dev/null +++ b/src/Modules/Ratings/Domain/Events/ReviewCreatedDomainEvent.cs @@ -0,0 +1,15 @@ +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Ratings.Domain.Events; + +/// +/// Evento de domínio disparado quando uma nova avaliação é criada. +/// +public record ReviewCreatedDomainEvent( + Guid AggregateId, + int Version, + Guid ProviderId, + Guid CustomerId, + int Rating, + string? Comment +) : DomainEvent(AggregateId, Version); diff --git a/src/Modules/Ratings/Domain/Events/ReviewRejectedDomainEvent.cs b/src/Modules/Ratings/Domain/Events/ReviewRejectedDomainEvent.cs new file mode 100644 index 000000000..8425d2e9e --- /dev/null +++ b/src/Modules/Ratings/Domain/Events/ReviewRejectedDomainEvent.cs @@ -0,0 +1,13 @@ +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Modules.Ratings.Domain.Events; + +/// +/// Evento de domínio disparado quando uma avaliação é rejeitada. +/// +public record ReviewRejectedDomainEvent( + Guid AggregateId, + int Version, + Guid ProviderId, + string Reason +) : DomainEvent(AggregateId, Version); diff --git a/src/Modules/Ratings/Domain/Exceptions/DuplicateReviewException.cs b/src/Modules/Ratings/Domain/Exceptions/DuplicateReviewException.cs new file mode 100644 index 000000000..80c299de5 --- /dev/null +++ b/src/Modules/Ratings/Domain/Exceptions/DuplicateReviewException.cs @@ -0,0 +1,11 @@ +using MeAjudaAi.Shared.Exceptions; + +namespace MeAjudaAi.Modules.Ratings.Domain.Exceptions; + +public class DuplicateReviewException : BusinessRuleException +{ + public DuplicateReviewException(Guid providerId, Guid customerId) + : base("DuplicateReview", $"O cliente '{customerId}' já avaliou o prestador '{providerId}'.") + { + } +} diff --git a/src/Modules/Ratings/Domain/MeAjudaAi.Modules.Ratings.Domain.csproj b/src/Modules/Ratings/Domain/MeAjudaAi.Modules.Ratings.Domain.csproj new file mode 100644 index 000000000..6e86728ea --- /dev/null +++ b/src/Modules/Ratings/Domain/MeAjudaAi.Modules.Ratings.Domain.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + + + + + <_Parameter1>MeAjudaAi.Modules.Ratings.Tests + + + <_Parameter1>MeAjudaAi.Modules.Ratings.Infrastructure + + + <_Parameter1>MeAjudaAi.Modules.Ratings.Application + + + + + + + + diff --git a/src/Modules/Ratings/Domain/Repositories/IReviewRepository.cs b/src/Modules/Ratings/Domain/Repositories/IReviewRepository.cs new file mode 100644 index 000000000..5a84849bf --- /dev/null +++ b/src/Modules/Ratings/Domain/Repositories/IReviewRepository.cs @@ -0,0 +1,25 @@ +using MeAjudaAi.Modules.Ratings.Domain.Entities; +using MeAjudaAi.Modules.Ratings.Domain.ValueObjects; + +namespace MeAjudaAi.Modules.Ratings.Domain.Repositories; + +/// +/// Repositório para a entidade Review. +/// +public interface IReviewRepository +{ + Task AddAsync(Review review, CancellationToken cancellationToken = default); + Task GetByIdAsync(ReviewId id, CancellationToken cancellationToken = default); + Task> GetByProviderIdAsync(Guid providerId, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default); + Task UpdateAsync(Review review, CancellationToken cancellationToken = default); + + /// + /// Recupera uma avaliação específica de um cliente para um prestador. + /// + Task GetByProviderAndCustomerAsync(Guid providerId, Guid customerId, CancellationToken cancellationToken = default); + + /// + /// Obtém a média de avaliações e total de reviews aprovados de um provedor. + /// + Task<(decimal AverageRating, int TotalReviews)> GetAverageRatingForProviderAsync(Guid providerId, CancellationToken cancellationToken = default); +} diff --git a/src/Modules/Ratings/Domain/ValueObjects/ReviewId.cs b/src/Modules/Ratings/Domain/ValueObjects/ReviewId.cs new file mode 100644 index 000000000..ffc39736e --- /dev/null +++ b/src/Modules/Ratings/Domain/ValueObjects/ReviewId.cs @@ -0,0 +1,34 @@ +using MeAjudaAi.Shared.Domain; +using MeAjudaAi.Shared.Utilities; + +namespace MeAjudaAi.Modules.Ratings.Domain.ValueObjects; + +/// +/// Identificador único da avaliação. +/// +public class ReviewId : ValueObject +{ + public Guid Value { get; } + + public ReviewId(Guid value) + { + if (value == Guid.Empty) + throw new ArgumentException("ReviewId não pode ser vazio"); + Value = value; + } + + public static ReviewId New() => new(UuidGenerator.NewId()); + + protected override IEnumerable GetEqualityComponents() + { + yield return Value; + } + + public static implicit operator Guid(ReviewId reviewId) + { + ArgumentNullException.ThrowIfNull(reviewId); + return reviewId.Value; + } + + public static implicit operator ReviewId(Guid guid) => new(guid); +} diff --git a/src/Modules/Ratings/Domain/packages.lock.json b/src/Modules/Ratings/Domain/packages.lock.json new file mode 100644 index 000000000..f87ef7ed4 --- /dev/null +++ b/src/Modules/Ratings/Domain/packages.lock.json @@ -0,0 +1,821 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "SonarAnalyzer.CSharp": { + "type": "Direct", + "requested": "[10.23.0.137933, )", + "resolved": "10.23.0.137933", + "contentHash": "K8CwUkB4PlUcMfLqGCj1aviVfW9AufZH2K/TjgPGYdj4gosV/kcyxpBR0jaCG8argC6dfxJBbSS6xXJ59lFWgA==" + }, + "Asp.Versioning.Abstractions": { + "type": "Transitive", + "resolved": "8.1.0", + "contentHash": "mpeNZyMdvrHztJwR1sXIUQ+3iioEU97YMBnFA9WLbsPOYhGwDJnqJMmEd8ny7kcmS9OjTHoEuX/bSXXY3brIFA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Dapper.AOT": { + "type": "Transitive", + "resolved": "1.0.48", + "contentHash": "rsLM3yKr4g+YKKox9lhc8D+kz67P7Q9+xdyn1LmCsoYr1kYpJSm+Nt6slo5UrfUrcTiGJ57zUlyO8XUdV7G7iA==" + }, + "Hangfire.NetCore": { + "type": "Transitive", + "resolved": "1.8.23", + "contentHash": "SmvUJF/u5MCP666R5Y1V+GntqBc4RCWJqn5ztMMN67d53Cx5cuaWR0YNLMrabjylwLarFYJ7EdR9RnGEZzp/dg==", + "dependencies": { + "Hangfire.Core": "[1.8.23]", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "3.0.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Bcl.TimeProvider": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.11.31", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Microsoft.VisualStudio.SolutionPersistence": "1.0.52", + "Newtonsoft.Json": "13.0.3", + "System.Composition": "9.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "zLgN22Zp9pk8RHlwssRTexw4+a6wqOnKWN+VejdPn5Yhjql4XiBhkFo35Nu8mmqHIk/UEmmCnMGLWq75aFfkOw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.11", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.1", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "So3JUdRxozRjvQ3cxU6F3nI/i4emDnjane6yMYcJhvTTTu29ltlIdoXjkFGRceIWz8yKvuEpzXItZ0x5GvN2nQ==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.FeatureManagement": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "qxvGAv9WJHYfOpixWywJTa1WNTPy5MbQiv+O+UlE6E/LVofiM1+YRR6m41zsHIbAGm1S0PQ0QFuAsOw9DkoKsg==", + "dependencies": { + "Microsoft.Bcl.TimeProvider": "8.0.1", + "Microsoft.Extensions.Caching.Memory": "8.0.1", + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.2", + "Microsoft.Extensions.Logging": "8.0.1" + } + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "Pipelines.Sockets.Unofficial": { + "type": "Transitive", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "StackExchange.Redis": { + "type": "Transitive", + "resolved": "2.7.27", + "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "meajudaai.contracts": { + "type": "Project", + "dependencies": { + "FluentValidation": "[12.1.1, )" + } + }, + "meajudaai.shared": { + "type": "Project", + "dependencies": { + "Asp.Versioning.Mvc": "[8.1.1, )", + "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )", + "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )", + "AspNetCore.HealthChecks.Redis": "[9.0.0, )", + "Dapper": "[2.1.72, )", + "EFCore.NamingConventions": "[10.0.1, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Hangfire.AspNetCore": "[1.8.23, )", + "Hangfire.Core": "[1.8.23, )", + "Hangfire.PostgreSql": "[1.21.1, )", + "MeAjudaAi.Contracts": "[1.0.0, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )", + "Microsoft.FeatureManagement.AspNetCore": "[4.4.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "RabbitMQ.Client": "[7.2.1, )", + "Rebus": "[8.9.0, )", + "Rebus.RabbitMq": "[10.1.1, )", + "Rebus.ServiceProvider": "[10.7.2, )", + "Scrutor": "[7.0.0, )", + "Serilog": "[4.3.1, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Enrichers.Environment": "[3.0.1, )", + "Serilog.Enrichers.Process": "[3.0.0, )", + "Serilog.Enrichers.Thread": "[4.0.0, )", + "Serilog.Settings.Configuration": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.Seq": "[9.0.0, )" + } + }, + "Asp.Versioning.Http": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "1D/Mzq1MSUSQe1eCY6GeEsu+tIlpzoWZxZkhlcw/uvtVoTrwO+BMl0fSj/XX8oZN1DutWTZqHDHgyDRq+IeSdQ==", + "dependencies": { + "Asp.Versioning.Abstractions": "8.1.0" + } + }, + "Asp.Versioning.Mvc": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "mkJv6eAdlbHbqTdrUcfEYFoZuGL6HoR7O+Lfsvivixp7N5BNhfCFPPOwsBzdIiH1qzdJXyJf+C+DZ08j27PMPg==", + "dependencies": { + "Asp.Versioning.Http": "8.1.1" + } + }, + "Asp.Versioning.Mvc.ApiExplorer": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "u8PrP6CjmurgIh0EDfg8Gc/GdpDHgkbv8OLKdxQcWb5W4sp0i9BcdbQlLvRcATjNIJUyr7ZmqXjmdsFfqnuy0g==", + "dependencies": { + "Asp.Versioning.Mvc": "8.1.1" + } + }, + "AspNetCore.HealthChecks.NpgSql": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "Npgsql": "8.0.3" + } + }, + "AspNetCore.HealthChecks.Redis": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "StackExchange.Redis": "2.7.4" + } + }, + "Dapper": { + "type": "CentralTransitive", + "requested": "[2.1.72, )", + "resolved": "2.1.72", + "contentHash": "ns4mGqQd9a/MhP8m6w556vVlZIa0/MfUu03zrxjZC/jlr1uVCsUac8bkdB+Fs98Llbd56rRSo1eZH5VVmeGZyw==" + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0" + } + }, + "Hangfire.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "TXpOl7kX4xXq5bLEqqWCpt9zh3TaouDwtb3GDtzGHX5uSC2RaAqZzn2swevivx3Uki16slXIigiPtgr4TPKpsg==", + "dependencies": { + "Hangfire.NetCore": "[1.8.23]" + } + }, + "Hangfire.Core": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "YCOTtF3NNOQI83PlfjeNDDBkofJDfdET2CwhfQsiVBwmsU6lP19QW9NVTIH9epl+MnOsyFC2G1RnlPSGV8F1FQ==", + "dependencies": { + "Newtonsoft.Json": "11.0.1" + } + }, + "Hangfire.PostgreSql": { + "type": "CentralTransitive", + "requested": "[1.21.1, )", + "resolved": "1.21.1", + "contentHash": "hFNZAxv+1p72/XCZdImnH6ovCzZ2DKAMTOI8CReT0P3yw/k0b0YJP2teA18agNH1ZYInPzhtxGk8hx5n2cxbbQ==", + "dependencies": { + "Dapper": "2.0.123", + "Dapper.AOT": "1.0.48", + "Hangfire.Core": "1.8.0", + "Npgsql": "6.0.11" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.Build.Framework": { + "type": "CentralTransitive", + "requested": "[18.0.2, )", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Hybrid": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.4", + "Microsoft.Extensions.Caching.Memory": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.StackExchangeRedis": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "StackExchange.Redis": "2.7.27" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Options": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.FeatureManagement.AspNetCore": { + "type": "CentralTransitive", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "jE5KxeEDUnUsx1w9uOFV5cOfM4E2mg7kf07328xO1x4JdoS0jwkD55nMjTlUPu90ynYX1oCBGC+FHK6ZoqRpxA==", + "dependencies": { + "Microsoft.FeatureManagement": "4.4.0" + } + }, + "Microsoft.OpenApi": { + "type": "CentralTransitive", + "requested": "[2.7.0, )", + "resolved": "2.7.0", + "contentHash": "b9xmpnmjq6p+HqF3uWG7u7/PlB38t/UB5UtXdi6xEAP9ZJGKHneYyjMGzBflB1rpLxYEcU6KRme+cz5wNPlxqA==" + }, + "Npgsql": { + "type": "CentralTransitive", + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Rebus": { + "type": "CentralTransitive", + "requested": "[8.9.0, )", + "resolved": "8.9.0", + "contentHash": "UaPGZuXIL4J5GUDA05JzEEzuPMEXY0CoF92nC6bsFBPvwoYPQ0uKyH2vKqdV80CW7cjbwBgDlEZ7R9hO9b59XA==", + "dependencies": { + "Newtonsoft.Json": "13.0.4" + } + }, + "Rebus.RabbitMq": { + "type": "CentralTransitive", + "requested": "[10.1.1, )", + "resolved": "10.1.1", + "contentHash": "66pUp4hfaYWfQEDOiVcuZQnPF4XFHyJ5KCfwCm18e3Dnr936Iog48KrN8Mp8QyRQ2tiNpzdjSATQLKEZpSk11A==", + "dependencies": { + "RabbitMq.Client": "7.1.2", + "rebus": "8.9.0" + } + }, + "Rebus.ServiceProvider": { + "type": "CentralTransitive", + "requested": "[10.7.2, )", + "resolved": "10.7.2", + "contentHash": "Qa8sKt1i9Fy/zCw5GwAUsfT+lt4BvkIgYh8sRJ6fvqJWoedS//pfcyiKUUb0wL3C5Wrpi3U+vRud5DCbMHaFIw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[8.0.0, 11.0.0)", + "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, 11.0.0)", + "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, 11.0.0)", + "Rebus": "8.9.0" + } + }, + "Scrutor": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "wHWaroody48jnlLoq/REwUltIFLxplXyHTP+sttrc8P7+jkiVqf38afDidJNv4qgD/6zz2NKOYg06xLZ1bG7wQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0" + } + }, + "Serilog": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Enrichers.Environment": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "9BqCE4C9FF+/rJb/CsQwe7oVf44xqkOvMwX//CUxvUR25lFL4tSS6iuxE5eW07quby1BAyAEP+vM6TWsnT3iqw==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Process": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "/wPYz2PDCJGSHNI+Z0PAacZvrgZgrGduWqLXeC2wvW6pgGM/Bi45JrKy887MRcRPHIZVU0LAlkmJ7TkByC0boQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Thread": { + "type": "CentralTransitive", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "C7BK25a1rhUyr+Tp+1BYcVlBJq7M2VCHlIgnwoIUVJcicM9jYcvQK18+OeHiXw7uLPSjqWxJIp1EfaZ/RGmEwA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Seq": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==", + "dependencies": { + "Serilog": "4.2.0", + "Serilog.Sinks.File": "6.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Ratings/Infrastructure/Events/Handlers/ReviewApprovedDomainEventHandler.cs b/src/Modules/Ratings/Infrastructure/Events/Handlers/ReviewApprovedDomainEventHandler.cs new file mode 100644 index 000000000..16c5cea53 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Events/Handlers/ReviewApprovedDomainEventHandler.cs @@ -0,0 +1,46 @@ +using MeAjudaAi.Modules.Ratings.Domain.Events; +using MeAjudaAi.Modules.Ratings.Domain.Repositories; +using MeAjudaAi.Shared.Events; +using MeAjudaAi.Shared.Messaging; +using MeAjudaAi.Shared.Messaging.Messages.Ratings; +using Microsoft.Extensions.Logging; + +namespace MeAjudaAi.Modules.Ratings.Infrastructure.Events.Handlers; + +public sealed class ReviewApprovedDomainEventHandler( + IMessageBus messageBus, + IReviewRepository repository, + ILogger logger) : IEventHandler +{ + public async Task HandleAsync(ReviewApprovedDomainEvent domainEvent, CancellationToken cancellationToken = default) + { + try + { + logger.LogInformation("Handling ReviewApprovedDomainEvent for provider {ProviderId}", domainEvent.ProviderId); + + // Calcula a nova média + var (average, total) = await repository.GetAverageRatingForProviderAsync(domainEvent.ProviderId, cancellationToken); + + var integrationEvent = new ReviewApprovedIntegrationEvent( + Source: "Ratings", + ProviderId: domainEvent.ProviderId, + ReviewId: domainEvent.AggregateId, + NewAverageRating: average, + TotalReviews: total, + ReviewRating: domainEvent.Rating, + ReviewComment: domainEvent.Comment, + CreatedAt: DateTime.UtcNow + ); + + await messageBus.PublishAsync(integrationEvent, cancellationToken: cancellationToken); + + logger.LogInformation("Successfully published ReviewApprovedIntegrationEvent for provider {ProviderId} (Average: {Average}, Total: {Total})", + domainEvent.ProviderId, average, total); + } + catch (Exception ex) + { + logger.LogError(ex, "Error handling ReviewApprovedDomainEvent for provider {ProviderId}", domainEvent.ProviderId); + throw; + } + } +} diff --git a/src/Modules/Ratings/Infrastructure/Events/Handlers/ReviewRejectedDomainEventHandler.cs b/src/Modules/Ratings/Infrastructure/Events/Handlers/ReviewRejectedDomainEventHandler.cs new file mode 100644 index 000000000..3b34790a5 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Events/Handlers/ReviewRejectedDomainEventHandler.cs @@ -0,0 +1,24 @@ +using MeAjudaAi.Modules.Ratings.Domain.Events; +using MeAjudaAi.Shared.Events; +using Microsoft.Extensions.Logging; + +namespace MeAjudaAi.Modules.Ratings.Infrastructure.Events.Handlers; + +public sealed class ReviewRejectedDomainEventHandler( + ILogger logger) : IEventHandler +{ + public Task HandleAsync(ReviewRejectedDomainEvent domainEvent, CancellationToken cancellationToken = default) + { + logger.LogWarning("Review {ReviewId} for provider {ProviderId} was rejected.", + domainEvent.AggregateId, domainEvent.ProviderId); + + var reasonPreview = domainEvent.Reason?.Length > 100 + ? domainEvent.Reason[..100] + "..." + : domainEvent.Reason; + + logger.LogDebug("Rejection reason for Review {ReviewId}: {Reason}", + domainEvent.AggregateId, reasonPreview); + + return Task.CompletedTask; + } +} diff --git a/src/Modules/Ratings/Infrastructure/Extensions.cs b/src/Modules/Ratings/Infrastructure/Extensions.cs new file mode 100644 index 000000000..3f0b462f8 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Extensions.cs @@ -0,0 +1,39 @@ +using MeAjudaAi.Modules.Ratings.Domain.Repositories; +using MeAjudaAi.Modules.Ratings.Infrastructure.Persistence; +using MeAjudaAi.Modules.Ratings.Infrastructure.Persistence.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace MeAjudaAi.Modules.Ratings.Infrastructure; + +public static class Extensions +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment) + { + services.AddDbContext((serviceProvider, options) => + { + var resolvedConfig = serviceProvider.GetRequiredService(); + var connStr = resolvedConfig.GetConnectionString("DefaultConnection") ?? + resolvedConfig.GetConnectionString("Ratings") ?? + resolvedConfig.GetConnectionString("meajudaai-db"); + + if (string.IsNullOrWhiteSpace(connStr) && MeAjudaAi.Shared.Utilities.EnvironmentHelpers.IsSecurityBypassEnvironment(environment)) + { +#pragma warning disable S2068 + connStr = "Host=localhost;Port=5432;Database=meajudaai_test;Username=postgres;Password=test"; +#pragma warning restore S2068 + } + + if (!string.IsNullOrWhiteSpace(connStr)) + { + options.UseNpgsql(connStr, m => m.MigrationsHistoryTable("__EFMigrationsHistory", "ratings")); + } + }); + + services.AddScoped(); + + return services; + } +} diff --git a/src/Modules/Ratings/Infrastructure/MeAjudaAi.Modules.Ratings.Infrastructure.csproj b/src/Modules/Ratings/Infrastructure/MeAjudaAi.Modules.Ratings.Infrastructure.csproj new file mode 100644 index 000000000..0dc1304a4 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/MeAjudaAi.Modules.Ratings.Infrastructure.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + + + + + <_Parameter1>MeAjudaAi.Modules.Ratings.Tests + + + + + + + + + + + + + + diff --git a/src/Modules/Ratings/Infrastructure/Persistence/Configurations/ReviewConfiguration.cs b/src/Modules/Ratings/Infrastructure/Persistence/Configurations/ReviewConfiguration.cs new file mode 100644 index 000000000..f13b2e5f2 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Persistence/Configurations/ReviewConfiguration.cs @@ -0,0 +1,64 @@ +using MeAjudaAi.Modules.Ratings.Domain.Entities; +using MeAjudaAi.Modules.Ratings.Domain.Enums; +using MeAjudaAi.Modules.Ratings.Domain.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace MeAjudaAi.Modules.Ratings.Infrastructure.Persistence.Configurations; + +public class ReviewConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("reviews"); + + builder.HasKey(r => r.Id); + + builder.Property(r => r.Id) + .HasConversion(id => id.Value, value => new ReviewId(value)) + .HasColumnName("id"); + + builder.Property(r => r.ProviderId) + .IsRequired() + .HasColumnName("provider_id"); + + builder.Property(r => r.CustomerId) + .IsRequired() + .HasColumnName("customer_id"); + + builder.Property(r => r.Rating) + .IsRequired() + .HasColumnName("rating"); + + builder.Property(r => r.Comment) + .HasMaxLength(1000) + .HasColumnName("comment"); + + builder.Property(r => r.Status) + .HasConversion( + status => status.ToString(), + value => Enum.Parse(value)) + .HasMaxLength(20) + .IsRequired() + .HasColumnName("status"); + + builder.Property(r => r.RejectionReason) + .HasMaxLength(500) + .HasColumnName("rejection_reason"); + + builder.Property(r => r.CreatedAt) + .IsRequired() + .HasColumnName("created_at"); + + builder.Property(r => r.UpdatedAt) + .HasColumnName("updated_at"); + + // Índices para performance e integridade + builder.HasIndex(r => r.ProviderId); + builder.HasIndex(r => r.CustomerId); + builder.HasIndex(r => r.Status); + + // Unicidade: Um cliente só pode avaliar um prestador uma vez + builder.HasIndex(r => new { r.ProviderId, r.CustomerId }).IsUnique(); + } +} diff --git a/src/Modules/Ratings/Infrastructure/Persistence/Migrations/20260413211313_InitialRatings.Designer.cs b/src/Modules/Ratings/Infrastructure/Persistence/Migrations/20260413211313_InitialRatings.Designer.cs new file mode 100644 index 000000000..20e13e025 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Persistence/Migrations/20260413211313_InitialRatings.Designer.cs @@ -0,0 +1,87 @@ +// +using System; +using MeAjudaAi.Modules.Ratings.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.Ratings.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(RatingsDbContext))] + [Migration("20260413211313_InitialRatings")] + partial class InitialRatings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("ratings") + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MeAjudaAi.Modules.Ratings.Domain.Entities.Review", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("comment"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone") + .HasColumnName("created_at"); + + b.Property("CustomerId") + .HasColumnType("uuid") + .HasColumnName("customer_id"); + + b.Property("ProviderId") + .HasColumnType("uuid") + .HasColumnName("provider_id"); + + b.Property("Rating") + .HasColumnType("integer") + .HasColumnName("rating"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("rejection_reason"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Status"); + + b.HasIndex("ProviderId", "CustomerId") + .IsUnique(); + + b.ToTable("reviews", "ratings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Ratings/Infrastructure/Persistence/Migrations/20260413211313_InitialRatings.cs b/src/Modules/Ratings/Infrastructure/Persistence/Migrations/20260413211313_InitialRatings.cs new file mode 100644 index 000000000..8dbf88683 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Persistence/Migrations/20260413211313_InitialRatings.cs @@ -0,0 +1,71 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MeAjudaAi.Modules.Ratings.Infrastructure.Persistence.Migrations +{ + /// + public partial class InitialRatings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "ratings"); + + migrationBuilder.CreateTable( + name: "reviews", + schema: "ratings", + columns: table => new + { + id = table.Column(type: "uuid", nullable: false), + provider_id = table.Column(type: "uuid", nullable: false), + customer_id = table.Column(type: "uuid", nullable: false), + rating = table.Column(type: "integer", nullable: false), + comment = table.Column(type: "character varying(1000)", maxLength: 1000, nullable: true), + status = table.Column(type: "character varying(20)", maxLength: 20, nullable: false), + rejection_reason = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + created_at = table.Column(type: "timestamp without time zone", nullable: false), + updated_at = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_reviews", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "IX_reviews_customer_id", + schema: "ratings", + table: "reviews", + column: "customer_id"); + + migrationBuilder.CreateIndex( + name: "IX_reviews_provider_id", + schema: "ratings", + table: "reviews", + column: "provider_id"); + + migrationBuilder.CreateIndex( + name: "IX_reviews_provider_id_customer_id", + schema: "ratings", + table: "reviews", + columns: new[] { "provider_id", "customer_id" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_reviews_status", + schema: "ratings", + table: "reviews", + column: "status"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "reviews", + schema: "ratings"); + } + } +} diff --git a/src/Modules/Ratings/Infrastructure/Persistence/Migrations/RatingsDbContextModelSnapshot.cs b/src/Modules/Ratings/Infrastructure/Persistence/Migrations/RatingsDbContextModelSnapshot.cs new file mode 100644 index 000000000..b4babc3aa --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Persistence/Migrations/RatingsDbContextModelSnapshot.cs @@ -0,0 +1,84 @@ +// +using System; +using MeAjudaAi.Modules.Ratings.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace MeAjudaAi.Modules.Ratings.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(RatingsDbContext))] + partial class RatingsDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("ratings") + .HasAnnotation("ProductVersion", "10.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MeAjudaAi.Modules.Ratings.Domain.Entities.Review", b => + { + b.Property("Id") + .HasColumnType("uuid") + .HasColumnName("id"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)") + .HasColumnName("comment"); + + b.Property("CreatedAt") + .HasColumnType("timestamp without time zone") + .HasColumnName("created_at"); + + b.Property("CustomerId") + .HasColumnType("uuid") + .HasColumnName("customer_id"); + + b.Property("ProviderId") + .HasColumnType("uuid") + .HasColumnName("provider_id"); + + b.Property("Rating") + .HasColumnType("integer") + .HasColumnName("rating"); + + b.Property("RejectionReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("rejection_reason"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("status"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp without time zone") + .HasColumnName("updated_at"); + + b.HasKey("Id"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Status"); + + b.HasIndex("ProviderId", "CustomerId") + .IsUnique(); + + b.ToTable("reviews", "ratings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Modules/Ratings/Infrastructure/Persistence/RatingsDbContext.cs b/src/Modules/Ratings/Infrastructure/Persistence/RatingsDbContext.cs new file mode 100644 index 000000000..d56e5c7f6 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Persistence/RatingsDbContext.cs @@ -0,0 +1,57 @@ +using System.Reflection; +using MeAjudaAi.Modules.Ratings.Domain.Entities; +using MeAjudaAi.Modules.Ratings.Domain.ValueObjects; +using MeAjudaAi.Shared.Database; +using MeAjudaAi.Shared.Domain; +using MeAjudaAi.Shared.Events; +using Microsoft.EntityFrameworkCore; + +namespace MeAjudaAi.Modules.Ratings.Infrastructure.Persistence; + +public class RatingsDbContext : BaseDbContext +{ + public RatingsDbContext(DbContextOptions options) + : base(options) + { + } + + public RatingsDbContext( + DbContextOptions options, + IDomainEventProcessor domainEventProcessor) + : base(options, domainEventProcessor) + { + } + + public DbSet Reviews { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("ratings"); + modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + base.OnModelCreating(modelBuilder); + } + + protected override Task> GetDomainEventsAsync(CancellationToken cancellationToken = default) + { + var domainEvents = ChangeTracker + .Entries>() + .Where(entry => entry.Entity.DomainEvents.Count > 0) + .SelectMany(entry => entry.Entity.DomainEvents) + .ToList(); + + return Task.FromResult(domainEvents); + } + + protected override void ClearDomainEvents() + { + var entities = ChangeTracker + .Entries>() + .Where(entry => entry.Entity.DomainEvents.Count > 0) + .Select(entry => entry.Entity); + + foreach (var entity in entities) + { + entity.ClearDomainEvents(); + } + } +} diff --git a/src/Modules/Ratings/Infrastructure/Persistence/Repositories/ReviewRepository.cs b/src/Modules/Ratings/Infrastructure/Persistence/Repositories/ReviewRepository.cs new file mode 100644 index 000000000..74ba9fd20 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/Persistence/Repositories/ReviewRepository.cs @@ -0,0 +1,66 @@ +using MeAjudaAi.Modules.Ratings.Domain.Entities; +using MeAjudaAi.Modules.Ratings.Domain.Enums; +using MeAjudaAi.Modules.Ratings.Domain.Exceptions; +using MeAjudaAi.Modules.Ratings.Domain.Repositories; +using MeAjudaAi.Modules.Ratings.Domain.ValueObjects; +using Microsoft.EntityFrameworkCore; +using Npgsql; + +namespace MeAjudaAi.Modules.Ratings.Infrastructure.Persistence.Repositories; + +public class ReviewRepository(RatingsDbContext context) : IReviewRepository +{ + public async Task AddAsync(Review review, CancellationToken cancellationToken = default) + { + try + { + await context.Reviews.AddAsync(review, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) when (ex.InnerException is PostgresException { SqlState: "23505" }) + { + throw new DuplicateReviewException(review.ProviderId, review.CustomerId); + } + } + + public async Task GetByIdAsync(ReviewId id, CancellationToken cancellationToken = default) + { + return await context.Reviews.FirstOrDefaultAsync(r => r.Id == id, cancellationToken); + } + + public async Task> GetByProviderIdAsync(Guid providerId, int page = 1, int pageSize = 10, CancellationToken cancellationToken = default) + { + return await context.Reviews + .Where(r => r.ProviderId == providerId && r.Status == EReviewStatus.Approved) + .OrderByDescending(r => r.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + } + + public async Task UpdateAsync(Review review, CancellationToken cancellationToken = default) + { + context.Reviews.Update(review); + await context.SaveChangesAsync(cancellationToken); + } + + public async Task GetByProviderAndCustomerAsync(Guid providerId, Guid customerId, CancellationToken cancellationToken = default) + { + return await context.Reviews + .FirstOrDefaultAsync(r => r.ProviderId == providerId && r.CustomerId == customerId, cancellationToken); + } + + public async Task<(decimal AverageRating, int TotalReviews)> GetAverageRatingForProviderAsync(Guid providerId, CancellationToken cancellationToken = default) + { + var query = context.Reviews + .Where(r => r.ProviderId == providerId && r.Status == EReviewStatus.Approved); + + var total = await query.CountAsync(cancellationToken); + + if (total == 0) + return (0, 0); + + var average = await query.AverageAsync(r => r.Rating, cancellationToken); + return (Math.Round((decimal)average, 2), total); + } +} diff --git a/src/Modules/Ratings/Infrastructure/packages.lock.json b/src/Modules/Ratings/Infrastructure/packages.lock.json new file mode 100644 index 000000000..077b703d2 --- /dev/null +++ b/src/Modules/Ratings/Infrastructure/packages.lock.json @@ -0,0 +1,835 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "SonarAnalyzer.CSharp": { + "type": "Direct", + "requested": "[10.23.0.137933, )", + "resolved": "10.23.0.137933", + "contentHash": "K8CwUkB4PlUcMfLqGCj1aviVfW9AufZH2K/TjgPGYdj4gosV/kcyxpBR0jaCG8argC6dfxJBbSS6xXJ59lFWgA==" + }, + "Asp.Versioning.Abstractions": { + "type": "Transitive", + "resolved": "8.1.0", + "contentHash": "mpeNZyMdvrHztJwR1sXIUQ+3iioEU97YMBnFA9WLbsPOYhGwDJnqJMmEd8ny7kcmS9OjTHoEuX/bSXXY3brIFA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Dapper.AOT": { + "type": "Transitive", + "resolved": "1.0.48", + "contentHash": "rsLM3yKr4g+YKKox9lhc8D+kz67P7Q9+xdyn1LmCsoYr1kYpJSm+Nt6slo5UrfUrcTiGJ57zUlyO8XUdV7G7iA==" + }, + "Hangfire.NetCore": { + "type": "Transitive", + "resolved": "1.8.23", + "contentHash": "SmvUJF/u5MCP666R5Y1V+GntqBc4RCWJqn5ztMMN67d53Cx5cuaWR0YNLMrabjylwLarFYJ7EdR9RnGEZzp/dg==", + "dependencies": { + "Hangfire.Core": "[1.8.23]", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "3.0.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.Bcl.TimeProvider": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.11.31", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Microsoft.VisualStudio.SolutionPersistence": "1.0.52", + "Newtonsoft.Json": "13.0.3", + "System.Composition": "9.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "zLgN22Zp9pk8RHlwssRTexw4+a6wqOnKWN+VejdPn5Yhjql4XiBhkFo35Nu8mmqHIk/UEmmCnMGLWq75aFfkOw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "8.0.11", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.1", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "8.0.11", + "contentHash": "So3JUdRxozRjvQ3cxU6F3nI/i4emDnjane6yMYcJhvTTTu29ltlIdoXjkFGRceIWz8yKvuEpzXItZ0x5GvN2nQ==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.FeatureManagement": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "qxvGAv9WJHYfOpixWywJTa1WNTPy5MbQiv+O+UlE6E/LVofiM1+YRR6m41zsHIbAGm1S0PQ0QFuAsOw9DkoKsg==", + "dependencies": { + "Microsoft.Bcl.TimeProvider": "8.0.1", + "Microsoft.Extensions.Caching.Memory": "8.0.1", + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.2", + "Microsoft.Extensions.Logging": "8.0.1" + } + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "Pipelines.Sockets.Unofficial": { + "type": "Transitive", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==" + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "StackExchange.Redis": { + "type": "Transitive", + "resolved": "2.7.27", + "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "meajudaai.contracts": { + "type": "Project", + "dependencies": { + "FluentValidation": "[12.1.1, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.shared": { + "type": "Project", + "dependencies": { + "Asp.Versioning.Mvc": "[8.1.1, )", + "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )", + "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )", + "AspNetCore.HealthChecks.Redis": "[9.0.0, )", + "Dapper": "[2.1.72, )", + "EFCore.NamingConventions": "[10.0.1, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Hangfire.AspNetCore": "[1.8.23, )", + "Hangfire.Core": "[1.8.23, )", + "Hangfire.PostgreSql": "[1.21.1, )", + "MeAjudaAi.Contracts": "[1.0.0, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )", + "Microsoft.FeatureManagement.AspNetCore": "[4.4.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "RabbitMQ.Client": "[7.2.1, )", + "Rebus": "[8.9.0, )", + "Rebus.RabbitMq": "[10.1.1, )", + "Rebus.ServiceProvider": "[10.7.2, )", + "Scrutor": "[7.0.0, )", + "Serilog": "[4.3.1, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Enrichers.Environment": "[3.0.1, )", + "Serilog.Enrichers.Process": "[3.0.0, )", + "Serilog.Enrichers.Thread": "[4.0.0, )", + "Serilog.Settings.Configuration": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.Seq": "[9.0.0, )" + } + }, + "Asp.Versioning.Http": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "1D/Mzq1MSUSQe1eCY6GeEsu+tIlpzoWZxZkhlcw/uvtVoTrwO+BMl0fSj/XX8oZN1DutWTZqHDHgyDRq+IeSdQ==", + "dependencies": { + "Asp.Versioning.Abstractions": "8.1.0" + } + }, + "Asp.Versioning.Mvc": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "mkJv6eAdlbHbqTdrUcfEYFoZuGL6HoR7O+Lfsvivixp7N5BNhfCFPPOwsBzdIiH1qzdJXyJf+C+DZ08j27PMPg==", + "dependencies": { + "Asp.Versioning.Http": "8.1.1" + } + }, + "Asp.Versioning.Mvc.ApiExplorer": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "u8PrP6CjmurgIh0EDfg8Gc/GdpDHgkbv8OLKdxQcWb5W4sp0i9BcdbQlLvRcATjNIJUyr7ZmqXjmdsFfqnuy0g==", + "dependencies": { + "Asp.Versioning.Mvc": "8.1.1" + } + }, + "AspNetCore.HealthChecks.NpgSql": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "Npgsql": "8.0.3" + } + }, + "AspNetCore.HealthChecks.Redis": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "StackExchange.Redis": "2.7.4" + } + }, + "Dapper": { + "type": "CentralTransitive", + "requested": "[2.1.72, )", + "resolved": "2.1.72", + "contentHash": "ns4mGqQd9a/MhP8m6w556vVlZIa0/MfUu03zrxjZC/jlr1uVCsUac8bkdB+Fs98Llbd56rRSo1eZH5VVmeGZyw==" + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0" + } + }, + "Hangfire.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "TXpOl7kX4xXq5bLEqqWCpt9zh3TaouDwtb3GDtzGHX5uSC2RaAqZzn2swevivx3Uki16slXIigiPtgr4TPKpsg==", + "dependencies": { + "Hangfire.NetCore": "[1.8.23]" + } + }, + "Hangfire.Core": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "YCOTtF3NNOQI83PlfjeNDDBkofJDfdET2CwhfQsiVBwmsU6lP19QW9NVTIH9epl+MnOsyFC2G1RnlPSGV8F1FQ==", + "dependencies": { + "Newtonsoft.Json": "11.0.1" + } + }, + "Hangfire.PostgreSql": { + "type": "CentralTransitive", + "requested": "[1.21.1, )", + "resolved": "1.21.1", + "contentHash": "hFNZAxv+1p72/XCZdImnH6ovCzZ2DKAMTOI8CReT0P3yw/k0b0YJP2teA18agNH1ZYInPzhtxGk8hx5n2cxbbQ==", + "dependencies": { + "Dapper": "2.0.123", + "Dapper.AOT": "1.0.48", + "Hangfire.Core": "1.8.0", + "Npgsql": "6.0.11" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.Build.Framework": { + "type": "CentralTransitive", + "requested": "[18.0.2, )", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Hybrid": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.4", + "Microsoft.Extensions.Caching.Memory": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.StackExchangeRedis": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "StackExchange.Redis": "2.7.27" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Options": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.FeatureManagement.AspNetCore": { + "type": "CentralTransitive", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "jE5KxeEDUnUsx1w9uOFV5cOfM4E2mg7kf07328xO1x4JdoS0jwkD55nMjTlUPu90ynYX1oCBGC+FHK6ZoqRpxA==", + "dependencies": { + "Microsoft.FeatureManagement": "4.4.0" + } + }, + "Microsoft.OpenApi": { + "type": "CentralTransitive", + "requested": "[2.7.0, )", + "resolved": "2.7.0", + "contentHash": "b9xmpnmjq6p+HqF3uWG7u7/PlB38t/UB5UtXdi6xEAP9ZJGKHneYyjMGzBflB1rpLxYEcU6KRme+cz5wNPlxqA==" + }, + "Npgsql": { + "type": "CentralTransitive", + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Rebus": { + "type": "CentralTransitive", + "requested": "[8.9.0, )", + "resolved": "8.9.0", + "contentHash": "UaPGZuXIL4J5GUDA05JzEEzuPMEXY0CoF92nC6bsFBPvwoYPQ0uKyH2vKqdV80CW7cjbwBgDlEZ7R9hO9b59XA==", + "dependencies": { + "Newtonsoft.Json": "13.0.4" + } + }, + "Rebus.RabbitMq": { + "type": "CentralTransitive", + "requested": "[10.1.1, )", + "resolved": "10.1.1", + "contentHash": "66pUp4hfaYWfQEDOiVcuZQnPF4XFHyJ5KCfwCm18e3Dnr936Iog48KrN8Mp8QyRQ2tiNpzdjSATQLKEZpSk11A==", + "dependencies": { + "RabbitMq.Client": "7.1.2", + "rebus": "8.9.0" + } + }, + "Rebus.ServiceProvider": { + "type": "CentralTransitive", + "requested": "[10.7.2, )", + "resolved": "10.7.2", + "contentHash": "Qa8sKt1i9Fy/zCw5GwAUsfT+lt4BvkIgYh8sRJ6fvqJWoedS//pfcyiKUUb0wL3C5Wrpi3U+vRud5DCbMHaFIw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[8.0.0, 11.0.0)", + "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, 11.0.0)", + "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, 11.0.0)", + "Rebus": "8.9.0" + } + }, + "Scrutor": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "wHWaroody48jnlLoq/REwUltIFLxplXyHTP+sttrc8P7+jkiVqf38afDidJNv4qgD/6zz2NKOYg06xLZ1bG7wQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0" + } + }, + "Serilog": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Enrichers.Environment": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "9BqCE4C9FF+/rJb/CsQwe7oVf44xqkOvMwX//CUxvUR25lFL4tSS6iuxE5eW07quby1BAyAEP+vM6TWsnT3iqw==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Process": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "/wPYz2PDCJGSHNI+Z0PAacZvrgZgrGduWqLXeC2wvW6pgGM/Bi45JrKy887MRcRPHIZVU0LAlkmJ7TkByC0boQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Thread": { + "type": "CentralTransitive", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "C7BK25a1rhUyr+Tp+1BYcVlBJq7M2VCHlIgnwoIUVJcicM9jYcvQK18+OeHiXw7uLPSjqWxJIp1EfaZ/RGmEwA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Seq": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==", + "dependencies": { + "Serilog": "4.2.0", + "Serilog.Sinks.File": "6.0.0" + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/Ratings/Tests/Integration/Persistence/Repositories/ReviewRepositoryTests.cs b/src/Modules/Ratings/Tests/Integration/Persistence/Repositories/ReviewRepositoryTests.cs new file mode 100644 index 000000000..fe79e2137 --- /dev/null +++ b/src/Modules/Ratings/Tests/Integration/Persistence/Repositories/ReviewRepositoryTests.cs @@ -0,0 +1,199 @@ +using MeAjudaAi.Modules.Ratings.Domain.Entities; +using MeAjudaAi.Modules.Ratings.Domain.Enums; +using MeAjudaAi.Modules.Ratings.Infrastructure.Persistence; +using MeAjudaAi.Modules.Ratings.Infrastructure.Persistence.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Data.Sqlite; +using FluentAssertions; + +namespace MeAjudaAi.Modules.Ratings.Tests.Integration.Persistence.Repositories; + +public class ReviewRepositoryTests : IAsyncDisposable +{ + private readonly RatingsDbContext _context; + private readonly ReviewRepository _repository; + private readonly SqliteConnection _connection; + + public ReviewRepositoryTests() + { + _connection = new SqliteConnection("DataSource=:memory:"); + _connection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(_connection) + .Options; + + _context = new RatingsDbContext(options); + _context.Database.EnsureCreated(); + _repository = new ReviewRepository(_context); + } + + public async ValueTask DisposeAsync() + { + await _context.DisposeAsync(); + await _connection.DisposeAsync(); + } + + [Fact] + public async Task AddAsync_ShouldPersistReview() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 5, "Test"); + + // Act + await _repository.AddAsync(review); + + // Assert + var persisted = await _context.Reviews.FindAsync(review.Id); + persisted.Should().NotBeNull(); + persisted!.Rating.Should().Be(5); + } + + [Fact] + public async Task GetByProviderAndCustomerAsync_ShouldReturnCorrectReview() + { + // Arrange + var providerId = Guid.NewGuid(); + var customerId = Guid.NewGuid(); + var review = Review.Create(providerId, customerId, 4, "Test"); + await _repository.AddAsync(review); + + // Act + var result = await _repository.GetByProviderAndCustomerAsync(providerId, customerId); + + // Assert + result.Should().NotBeNull(); + result!.Id.Should().Be(review.Id); + } + + [Fact] + public async Task GetByIdAsync_WithValidId_ShouldReturnReview() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 5, "Test"); + await _repository.AddAsync(review); + + // Act + var result = await _repository.GetByIdAsync(review.Id); + + // Assert + result.Should().NotBeNull(); + result!.Id.Should().Be(review.Id); + } + + [Fact] + public async Task GetByProviderIdAsync_WithApprovedReviews_ShouldReturnPaginatedAndOrdered() + { + // Arrange + var providerId = Guid.NewGuid(); + var r1 = Review.Create(providerId, Guid.NewGuid(), 4, "Oldest"); + r1.Approve(); + + await Task.Delay(20); // Garantir timestamps diferentes (SQLite pode ser rápido demais) + + var r2 = Review.Create(providerId, Guid.NewGuid(), 5, "Latest"); + r2.Approve(); + + var r3 = Review.Create(providerId, Guid.NewGuid(), 1, "Rejected"); + r3.Reject("Bad"); + + await _repository.AddAsync(r1); + await _repository.AddAsync(r2); + await _repository.AddAsync(r3); + + // Act + var result = (await _repository.GetByProviderIdAsync(providerId, 1, 10)).ToList(); + + // Assert + result.Should().HaveCount(2); + result[0].Comment.Should().Be("Latest"); + result[1].Comment.Should().Be("Oldest"); + } + + [Fact] + public async Task UpdateAsync_ShouldPersistChanges() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 5, "Test"); + await _repository.AddAsync(review); + review.Approve(); + + // Act + await _repository.UpdateAsync(review); + + // Assert + var persisted = await _context.Reviews.FindAsync(review.Id); + persisted!.Status.Should().Be(EReviewStatus.Approved); + } + + [Fact] + public async Task GetAverageRatingForProviderAsync_ShouldCalculateCorrectly() + { + // Arrange + var providerId = Guid.NewGuid(); + + var r1 = Review.Create(providerId, Guid.NewGuid(), 5, null); + r1.Approve(); + var r2 = Review.Create(providerId, Guid.NewGuid(), 4, null); + r2.Approve(); + var r3 = Review.Create(providerId, Guid.NewGuid(), 1, null); // Not approved + + await _repository.AddAsync(r1); + await _repository.AddAsync(r2); + await _repository.AddAsync(r3); + + // Act + var (average, total) = await _repository.GetAverageRatingForProviderAsync(providerId); + + // Assert + average.Should().Be(4.5m); + total.Should().Be(2); + } + + [Fact] + public async Task GetAverageRatingForProviderAsync_WhenNoApprovedReviews_ShouldReturnZero() + { + // Arrange + var providerId = Guid.NewGuid(); + var review = Review.Create(providerId, Guid.NewGuid(), 5, "Pending"); + await _repository.AddAsync(review); + + // Act + var (average, total) = await _repository.GetAverageRatingForProviderAsync(providerId); + + // Assert + average.Should().Be(0); + total.Should().Be(0); + } + + [Fact] + public async Task GetByProviderIdAsync_Pagination_ShouldReturnCorrectSubset() + { + // Arrange + var providerId = Guid.NewGuid(); + for (int i = 1; i <= 5; i++) + { + var r = Review.Create(providerId, Guid.NewGuid(), 5, $"Review {i}"); + r.Approve(); + await _repository.AddAsync(r); + } + + // Act + var page1 = await _repository.GetByProviderIdAsync(providerId, 1, 3); + var page2 = await _repository.GetByProviderIdAsync(providerId, 2, 3); + + // Assert + page1.Should().HaveCount(3); + page2.Should().HaveCount(2); + } + + [Fact] + public async Task GetByProviderIdAsync_WhenNoReviews_ShouldReturnEmpty() + { + // Act + var result = await _repository.GetByProviderIdAsync(Guid.NewGuid()); + + // Assert + result.Should().BeEmpty(); + } +} diff --git a/src/Modules/Ratings/Tests/MeAjudaAi.Modules.Ratings.Tests.csproj b/src/Modules/Ratings/Tests/MeAjudaAi.Modules.Ratings.Tests.csproj new file mode 100644 index 000000000..36f05e998 --- /dev/null +++ b/src/Modules/Ratings/Tests/MeAjudaAi.Modules.Ratings.Tests.csproj @@ -0,0 +1,57 @@ + + + + net10.0 + enable + enable + false + true + $(NoWarn);CA2201 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Modules/Ratings/Tests/Unit/Application/Handlers/CreateReviewCommandHandlerTests.cs b/src/Modules/Ratings/Tests/Unit/Application/Handlers/CreateReviewCommandHandlerTests.cs new file mode 100644 index 000000000..0e88db2c8 --- /dev/null +++ b/src/Modules/Ratings/Tests/Unit/Application/Handlers/CreateReviewCommandHandlerTests.cs @@ -0,0 +1,154 @@ +using MeAjudaAi.Modules.Ratings.Application.Commands; +using MeAjudaAi.Modules.Ratings.Application.Handlers; +using MeAjudaAi.Modules.Ratings.Application.Services; +using MeAjudaAi.Modules.Ratings.Domain.Entities; +using MeAjudaAi.Modules.Ratings.Domain.Enums; +using MeAjudaAi.Modules.Ratings.Domain.Repositories; +using Microsoft.Extensions.Logging; +using Moq; +using FluentAssertions; + +namespace MeAjudaAi.Modules.Ratings.Tests.Unit.Application.Handlers; + +public class CreateReviewCommandHandlerTests +{ + private readonly Mock _repositoryMock; + private readonly Mock _moderatorMock; + private readonly Mock> _loggerMock; + private readonly CreateReviewCommandHandler _handler; + + public CreateReviewCommandHandlerTests() + { + _repositoryMock = new Mock(); + _moderatorMock = new Mock(); + _loggerMock = new Mock>(); + + _moderatorMock.Setup(m => m.IsClean(It.IsAny())).Returns(true); + + _handler = new CreateReviewCommandHandler(_repositoryMock.Object, _moderatorMock.Object, _loggerMock.Object); + } + + [Fact] + public async Task HandleAsync_ValidCommand_ShouldAddReviewToRepository() + { + // Arrange + var command = new CreateReviewCommand(Guid.NewGuid(), Guid.NewGuid(), 5, "Great!"); + + // Act + var result = await _handler.HandleAsync(command); + + // Assert + result.Should().NotBeEmpty(); + _repositoryMock.Verify(r => r.AddAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_Rating5WithoutComment_ShouldAutoApprove() + { + // Arrange + var command = new CreateReviewCommand(Guid.NewGuid(), Guid.NewGuid(), 5, null); + + // Act + await _handler.HandleAsync(command); + + // Assert + _repositoryMock.Verify(r => r.AddAsync( + It.Is(rev => rev.Status == EReviewStatus.Approved), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_Rating4WithoutComment_ShouldAutoApprove() + { + // Arrange + var command = new CreateReviewCommand(Guid.NewGuid(), Guid.NewGuid(), 4, null); + + // Act + await _handler.HandleAsync(command); + + // Assert + _repositoryMock.Verify(r => r.AddAsync( + It.Is(rev => rev.Status == EReviewStatus.Approved), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_Rating5WithCleanComment_ShouldNotAutoApprove() + { + // Arrange + var command = new CreateReviewCommand(Guid.NewGuid(), Guid.NewGuid(), 5, "Great service!"); + _moderatorMock.Setup(m => m.IsClean(It.IsAny())).Returns(true); + + // Act + await _handler.HandleAsync(command); + + // Assert + _repositoryMock.Verify(r => r.AddAsync( + It.Is(rev => rev.Status == EReviewStatus.Pending), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_Rating3WithoutComment_ShouldNotAutoApprove() + { + // Arrange + var command = new CreateReviewCommand(Guid.NewGuid(), Guid.NewGuid(), 3, null); + + // Act + await _handler.HandleAsync(command); + + // Assert + _repositoryMock.Verify(r => r.AddAsync( + It.Is(rev => rev.Status == EReviewStatus.Pending), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_Rating5WithWhitespaceComment_ShouldAutoApprove() + { + // Arrange + var command = new CreateReviewCommand(Guid.NewGuid(), Guid.NewGuid(), 5, " "); + + // Act + await _handler.HandleAsync(command); + + // Assert + _repositoryMock.Verify(r => r.AddAsync( + It.Is(rev => rev.Status == EReviewStatus.Approved), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_DirtyContent_ShouldMarkAsFlagged() + { + // Arrange + var command = new CreateReviewCommand(Guid.NewGuid(), Guid.NewGuid(), 5, "Some bad word"); + _moderatorMock.Setup(m => m.IsClean(It.IsAny())).Returns(false); + + // Act + await _handler.HandleAsync(command); + + // Assert + _repositoryMock.Verify(r => r.AddAsync( + It.Is(rev => rev.Status == EReviewStatus.Flagged), + It.IsAny()), Times.Once); + } + + [Fact] + public async Task HandleAsync_DuplicateReview_ShouldThrowInvalidOperationException() + { + // Arrange + var providerId = Guid.NewGuid(); + var customerId = Guid.NewGuid(); + var command = new CreateReviewCommand(providerId, customerId, 5, null); + + _repositoryMock.Setup(r => r.GetByProviderAndCustomerAsync(providerId, customerId, It.IsAny())) + .ReturnsAsync(Review.Create(providerId, customerId, 1, "Existing")); + + // Act + Func act = () => _handler.HandleAsync(command); + + // Assert + await act.Should().ThrowAsync().WithMessage("Você já avaliou este prestador."); + } +} diff --git a/src/Modules/Ratings/Tests/Unit/Application/Services/ContentModeratorTests.cs b/src/Modules/Ratings/Tests/Unit/Application/Services/ContentModeratorTests.cs new file mode 100644 index 000000000..7bc91a8b4 --- /dev/null +++ b/src/Modules/Ratings/Tests/Unit/Application/Services/ContentModeratorTests.cs @@ -0,0 +1,52 @@ +using MeAjudaAi.Modules.Ratings.Application.Services; +using FluentAssertions; + +namespace MeAjudaAi.Modules.Ratings.Tests.Unit.Application.Services; + +public class ContentModeratorTests +{ + private readonly ContentModerator _moderator; + + public ContentModeratorTests() + { + _moderator = new ContentModerator(); + } + + [Theory] + [InlineData("Esse serviço é muito bom")] + [InlineData("Gostei bastante")] + [InlineData(null)] + [InlineData("")] + public void IsClean_WithCleanContent_ShouldReturnTrue(string? content) + { + // Act + var result = _moderator.IsClean(content); + + // Assert + result.Should().BeTrue(); + } + + [Theory] + [InlineData("Você é um idiota")] + [InlineData("Isso é um golpe")] + [InlineData("Que lixo de serviço")] + public void IsClean_WithDirtyContent_ShouldReturnFalse(string content) + { + // Act + var result = _moderator.IsClean(content); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void IsClean_WithPartialMatch_ShouldReturnTrue() + { + // Bad word is "burro", but "burro" inside another word like "sussurro" should be clean + // Act + var result = _moderator.IsClean("O sussurro do vento"); + + // Assert + result.Should().BeTrue(); + } +} diff --git a/src/Modules/Ratings/Tests/Unit/Domain/Entities/ReviewTests.cs b/src/Modules/Ratings/Tests/Unit/Domain/Entities/ReviewTests.cs new file mode 100644 index 000000000..b77099cef --- /dev/null +++ b/src/Modules/Ratings/Tests/Unit/Domain/Entities/ReviewTests.cs @@ -0,0 +1,190 @@ +using MeAjudaAi.Modules.Ratings.Domain.Entities; +using MeAjudaAi.Modules.Ratings.Domain.Enums; +using MeAjudaAi.Modules.Ratings.Domain.Events; +using FluentAssertions; + +namespace MeAjudaAi.Modules.Ratings.Tests.Unit.Domain.Entities; + +public class ReviewTests +{ + [Fact] + public void Create_WithValidData_ShouldCreateReviewAndAddDomainEvent() + { + // Arrange + var providerId = Guid.NewGuid(); + var customerId = Guid.NewGuid(); + var rating = 5; + var comment = "Excellent service!"; + + // Act + var review = Review.Create(providerId, customerId, rating, comment); + + // Assert + review.Should().NotBeNull(); + review.ProviderId.Should().Be(providerId); + review.CustomerId.Should().Be(customerId); + review.Rating.Should().Be(rating); + review.Comment.Should().Be(comment); + review.Status.Should().Be(EReviewStatus.Pending); + review.DomainEvents.Should().ContainSingle(e => e is ReviewCreatedDomainEvent); + } + + [Theory] + [InlineData(0)] + [InlineData(6)] + public void Create_WithInvalidRating_ShouldThrowArgumentOutOfRangeException(int invalidRating) + { + // Arrange + var providerId = Guid.NewGuid(); + var customerId = Guid.NewGuid(); + + // Act + Action action = () => Review.Create(providerId, customerId, invalidRating, "Comment"); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void Create_WithEmptyProviderId_ShouldThrowArgumentException() + { + // Act + Action action = () => Review.Create(Guid.Empty, Guid.NewGuid(), 5, "Comment"); + + // Assert + action.Should().Throw().WithMessage("*ProviderId*"); + } + + [Fact] + public void Create_WithEmptyCustomerId_ShouldThrowArgumentException() + { + // Act + Action action = () => Review.Create(Guid.NewGuid(), Guid.Empty, 5, "Comment"); + + // Assert + action.Should().Throw().WithMessage("*CustomerId*"); + } + + [Fact] + public void Approve_WhenPending_ShouldChangeStatusToApprovedAndAddDomainEvent() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 5, "Comment"); + + // Act + review.Approve(); + + // Assert + review.Status.Should().Be(EReviewStatus.Approved); + review.DomainEvents.OfType().Should().ContainSingle(); + } + + [Fact] + public void Approve_WhenAlreadyApproved_ShouldDoNothing() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 5, "Comment"); + review.Approve(); + review.ClearDomainEvents(); + + // Act + review.Approve(); + + // Assert + review.Status.Should().Be(EReviewStatus.Approved); + review.DomainEvents.Should().BeEmpty(); + } + + [Fact] + public void Approve_WhenPreviouslyRejected_ShouldClearRejectionReason() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 5, "Fixed"); + review.Reject("Bad content"); + + // Act + review.Approve(); + + // Assert + review.Status.Should().Be(EReviewStatus.Approved); + review.RejectionReason.Should().BeNull(); + } + + [Fact] + public void Reject_WithReason_ShouldChangeStatusToRejectedAndAddDomainEvent() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 1, "Spam"); + var reason = "Contains offensive language"; + + // Act + review.Reject(reason); + + // Assert + review.Status.Should().Be(EReviewStatus.Rejected); + review.RejectionReason.Should().Be(reason); + review.DomainEvents.OfType().Should().ContainSingle(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData(" ")] + public void Reject_WithInvalidReason_ShouldThrowArgumentException(string invalidReason) + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 1, "Comment"); + + // Act + Action action = () => review.Reject(invalidReason!); + + // Assert + action.Should().Throw().WithMessage("*Motivo*"); + } + + [Fact] + public void Reject_WhenAlreadyRejected_ShouldDoNothing() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 1, "Comment"); + review.Reject("Initial reason"); + review.ClearDomainEvents(); + + // Act + review.Reject("New reason"); + + // Assert + review.Status.Should().Be(EReviewStatus.Rejected); + review.RejectionReason.Should().Be("Initial reason"); + review.DomainEvents.Should().BeEmpty(); + } + + [Fact] + public void MarkAsFlagged_ShouldChangeStatusToFlagged() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 3, "Suspicious"); + + // Act + review.MarkAsFlagged(); + + // Assert + review.Status.Should().Be(EReviewStatus.Flagged); + } + + [Fact] + public void MarkAsFlagged_WhenAlreadyFlagged_ShouldDoNothing() + { + // Arrange + var review = Review.Create(Guid.NewGuid(), Guid.NewGuid(), 3, "Comment"); + review.MarkAsFlagged(); + review.ClearDomainEvents(); + + // Act + review.MarkAsFlagged(); + + // Assert + review.Status.Should().Be(EReviewStatus.Flagged); + review.DomainEvents.Should().BeEmpty(); + } +} diff --git a/src/Modules/Ratings/Tests/Unit/Domain/ValueObjects/ReviewIdTests.cs b/src/Modules/Ratings/Tests/Unit/Domain/ValueObjects/ReviewIdTests.cs new file mode 100644 index 000000000..38a958ed8 --- /dev/null +++ b/src/Modules/Ratings/Tests/Unit/Domain/ValueObjects/ReviewIdTests.cs @@ -0,0 +1,43 @@ +using MeAjudaAi.Modules.Ratings.Domain.ValueObjects; +using FluentAssertions; + +namespace MeAjudaAi.Modules.Ratings.Tests.Unit.Domain.ValueObjects; + +public class ReviewIdTests +{ + [Fact] + public void Constructor_WithEmptyGuid_ShouldThrowArgumentException() + { + // Act + Action action = () => new ReviewId(Guid.Empty); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void ImplicitConversion_FromGuid_ShouldCreateReviewId() + { + // Arrange + var guid = Guid.NewGuid(); + + // Act + ReviewId reviewId = guid; + + // Assert + reviewId.Value.Should().Be(guid); + } + + [Fact] + public void ImplicitConversion_ToGuid_ShouldReturnGuid() + { + // Arrange + var reviewId = ReviewId.New(); + + // Act + Guid guid = reviewId; + + // Assert + guid.Should().Be(reviewId.Value); + } +} diff --git a/src/Modules/Ratings/Tests/packages.lock.json b/src/Modules/Ratings/Tests/packages.lock.json new file mode 100644 index 000000000..523de0ada --- /dev/null +++ b/src/Modules/Ratings/Tests/packages.lock.json @@ -0,0 +1,2310 @@ +{ + "version": 2, + "dependencies": { + "net10.0": { + "AutoFixture": { + "type": "Direct", + "requested": "[4.18.1, )", + "resolved": "4.18.1", + "contentHash": "BmWZDY4fkrYOyd5/CTBOeXbzsNwV8kI4kDi/Ty1Y5F+WDHBVKxzfWlBE4RSicvZ+EOi2XDaN5uwdrHsItLW6Kw==", + "dependencies": { + "Fare": "[2.1.1, 3.0.0)" + } + }, + "AutoFixture.AutoMoq": { + "type": "Direct", + "requested": "[4.18.1, )", + "resolved": "4.18.1", + "contentHash": "5mG4BdhamHBJGDKNdH5p0o1GIqbNCDqq+4Ny4csnYpzZPYjkfT5xOXLyhkvpF8EgK3GN5o4HMclEe2rhQVr1jQ==", + "dependencies": { + "AutoFixture": "4.18.1", + "Moq": "[4.7.0, 5.0.0)" + } + }, + "Bogus": { + "type": "Direct", + "requested": "[35.6.5, )", + "resolved": "35.6.5", + "contentHash": "2FGZn+aAVHjmCgClgmGkTDBVZk0zkLvAKGaxEf5JL6b3i9JbHTE4wnuY4vHCuzlCmJdU6VZjgDfHwmYkQF8VAA==" + }, + "coverlet.collector": { + "type": "Direct", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "heVQl5tKYnnIDYlR1QMVGueYH6iriZTcZB6AjDczQNwZzxkjDIt9C84Pt4cCiZYrbo7jkZOYGWbs6Lo9wAtVLg==" + }, + "FluentAssertions": { + "type": "Direct", + "requested": "[8.9.0, )", + "resolved": "8.9.0", + "contentHash": "Y5RDjxaVlxWX2yy0X/ay1tJjSKMOtjepSb83mmfngFS63hm3LsoZNj6nhmImzm1ifRmpF9ouvmHjx9nNwnkpDg==" + }, + "Microsoft.AspNetCore.Mvc.Testing": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "MfacYQ7jNzj6073YobyoFfXpNmGqrV1UCywTM339DOcYpfalcM4K4heFjV5k3dDkKkWOGWO/DV3hdmVRqFkIxA==", + "dependencies": { + "Microsoft.AspNetCore.TestHost": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Hosting": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.InMemory": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "zz4THzlDOrJ7fKU2YUOzQFs2LJ9DgOSr5xFXcDdoD59el73MTQLtQQOAJxQ94F4XDegyL9+2sePSQGdcU25ZxQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Direct", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "lxeRviglTkkmzYJVJ600yb6gJjnf5za9v7uH+0byuSXTGv7U8cT6hz7qRTmiGSOfLcl86QFdy2BBKaUFd6NQug==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.bundle_e_sqlite3": "2.1.11", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[18.4.0, )", + "resolved": "18.4.0", + "contentHash": "w49iZdL4HL6V25l41NVQLXWQ+e71GvSkKVteMrOL02gP/PUkcnO/1yEb2s9FntU4wGmJWfKnyrRAhcMHd9ZZNA==", + "dependencies": { + "Microsoft.CodeCoverage": "18.4.0", + "Microsoft.TestPlatform.TestHost": "18.4.0" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.20.72, )", + "resolved": "4.20.72", + "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", + "dependencies": { + "Castle.Core": "5.1.1" + } + }, + "Npgsql": { + "type": "Direct", + "requested": "[10.0.2, )", + "resolved": "10.0.2", + "contentHash": "q5RfBI+wywJSFUNDE1L4ZbHEHCFTblo8Uf6A6oe4feOUFYiUQXyAf9GBh5qEZpvJaHiEbpBPkQumjEhXCJxdrg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "10.0.0" + } + }, + "Respawn": { + "type": "Direct", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "AyWlducZGOnmlEVz4PaL3Qv8wcY3ClPZS9CrlDnSVahGd/E9tTEgSFiC8yoV/F6o6P6IYm8xnHFa/vmWT8tfcw==" + }, + "Testcontainers.PostgreSql": { + "type": "Direct", + "requested": "[4.11.0, )", + "resolved": "4.11.0", + "contentHash": "OWGi0Og+qFpr2OPDignA74aJSfUd0nvZOaXNGWXwMJR1BvpdzhCNHQB2h7yLSTb0a8JVmmQQ4mUpHAC9q27NLw==", + "dependencies": { + "Testcontainers": "4.11.0" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" + }, + "xunit.v3": { + "type": "Direct", + "requested": "[3.2.2, )", + "resolved": "3.2.2", + "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", + "dependencies": { + "xunit.v3.mtp-v1": "[3.2.2]" + } + }, + "Asp.Versioning.Abstractions": { + "type": "Transitive", + "resolved": "8.1.0", + "contentHash": "mpeNZyMdvrHztJwR1sXIUQ+3iioEU97YMBnFA9WLbsPOYhGwDJnqJMmEd8ny7kcmS9OjTHoEuX/bSXXY3brIFA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "AspNetCore.HealthChecks.UI.Core": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "TVriy4hgYnhfqz6NAzv8qe62Q8wf82iKUL6WV9selqeFZTq1ILi39Sic6sFQegRysvAVcnxKP/vY8z9Fk8x6XQ==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11" + } + }, + "Azure.Core": { + "type": "Transitive", + "resolved": "1.50.0", + "contentHash": "GBNKZEhdIbTXxedvD3R7I/yDVFX9jJJEz02kCziFSJxspSQ5RMHc3GktulJ1s7+ffXaXD7kMgrtdQTaggyInLw==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "8.0.0", + "System.ClientModel": "1.8.0", + "System.Memory.Data": "8.0.1" + } + }, + "Azure.Monitor.OpenTelemetry.Exporter": { + "type": "Transitive", + "resolved": "1.5.0", + "contentHash": "7YgW82V13PwhjrlaN2Nbu9UIvYMzZxjgV9TYqK34PK+81IWsDwPO3vBhyeHYpDBwKWm7wqHp1c3VVX5DN4G2WA==", + "dependencies": { + "Azure.Core": "1.50.0", + "OpenTelemetry": "1.14.0", + "OpenTelemetry.Extensions.Hosting": "1.14.0", + "OpenTelemetry.PersistentStorage.FileSystem": "1.0.2" + } + }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.6.2", + "contentHash": "7oWOcvnntmMKNzDLsdxAYqApt+AjpRpP2CShjMfIa3umZ42UQMvH0tl1qAliYPNYO6vTdcGMqnRrCPmsfzTI1w==" + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "5.1.1", + "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", + "dependencies": { + "System.Diagnostics.EventLog": "6.0.0" + } + }, + "Dapper.AOT": { + "type": "Transitive", + "resolved": "1.0.48", + "contentHash": "rsLM3yKr4g+YKKox9lhc8D+kz67P7Q9+xdyn1LmCsoYr1kYpJSm+Nt6slo5UrfUrcTiGJ57zUlyO8XUdV7G7iA==" + }, + "Docker.DotNet.Enhanced": { + "type": "Transitive", + "resolved": "3.131.1", + "contentHash": "hGLHCNUsQbT2Ab/HUznRnNqYZQs40zInXa3eLwYjeNyfUYbw1pqqDGqcOLl5uGepS8IuigEYakEdAcVT/2ezYg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.3" + } + }, + "Docker.DotNet.Enhanced.X509": { + "type": "Transitive", + "resolved": "3.131.1", + "contentHash": "8FU7zmttFQzp0xb0EPupxQ0nGtC2cTpukgh3jMxMT8luj5TSDyzIKTnroDpXCjpg9P2fV+6JIvC+IetsMEfyBA==", + "dependencies": { + "Docker.DotNet.Enhanced": "3.131.1" + } + }, + "Fare": { + "type": "Transitive", + "resolved": "2.1.1", + "contentHash": "HaI8puqA66YU7/9cK4Sgbs1taUTP1Ssa4QT2PIzqJ7GvAbN1QgkjbRsjH+FSbMh1MJdvS0CIwQNLtFT+KF6KpA==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "Hangfire.NetCore": { + "type": "Transitive", + "resolved": "1.8.23", + "contentHash": "SmvUJF/u5MCP666R5Y1V+GntqBc4RCWJqn5ztMMN67d53Cx5cuaWR0YNLMrabjylwLarFYJ7EdR9RnGEZzp/dg==", + "dependencies": { + "Hangfire.Core": "[1.8.23]", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "3.0.0", + "Microsoft.Extensions.Logging.Abstractions": "3.0.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "Microsoft.ApplicationInsights": { + "type": "Transitive", + "resolved": "2.23.0", + "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3WA9q9yVqJp222P3x1wYIGDAkpjAku0TMUaaQV22g6L67AI0LdOIrVS7Ht2vJfLHGSPVuqN94vIr15qn+HEkHw==" + }, + "Microsoft.Bcl.TimeProvider": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "C7kWHJnMRY7EvJev2S8+yJHZ1y7A4ZlLbA4NE+O23BDIAN5mHeqND1m+SKv1ChRS5YlCDW7yAMUe7lttRsJaAA==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZXRAdvH6GiDeHRyd3q/km8Z44RoM6FBWHd+gen/la81mVnAdHTEsEkO5J0TCNXBymAcx5UYKt5TvgKBhaLJEow==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "5DSyJ9bk+ATuDy7fp2Zt0mJStDVKbBoiz1DyfAwSa+k4H4IwykAUcV3URelw5b8/iVbfSaOwkwmPUZH6opZKCw==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "Al/Q8B+yO8odSqGVpSvrShMFDvlQdIBU//F3E6Rb0YdiLSALE9wh/pvozPNnfmh5HDnvU+mkmSjpz4hQO++jaA==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.CSharp": "[5.0.0]", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "ZbUmIvT6lqTNKiv06Jl5wf0MTMi1vQ1oH7ou4CLcs2C/no/L7EhP3T8y3XXvn9VbqMcJaJnEsNA1jwYUMgc5jg==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[5.0.0]", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.MSBuild": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "/G+LVoAGMz6Ae8nm+PGLxSw+F5RjYx/J7irbTO5uKAPw1bxHyQJLc/YOnpDxt+EpPtYxvC9wvBsg/kETZp1F9Q==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "17.11.31", + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Workspaces.Common": "[5.0.0]", + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0", + "Microsoft.VisualStudio.SolutionPersistence": "1.0.52", + "Newtonsoft.Json": "13.0.3", + "System.Composition": "9.0.0" + } + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "18.4.0", + "contentHash": "9O0BtCfzCWrkAmK187ugKdq72HHOXoOUjuWFDVc2LsZZ0pOnA9bTt+Sg9q4cF+MoAaUU+MuWtvBuFsnduviJow==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jFYXnh7s0RShCw6Vkf+ReGCw+mVi7ISg1YaEzYCJcXnUifmbW+aqvCsRJuSRj2ZuQ+oqetpjxlZtbpMmk5FKqQ==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "32c58Rnm47Qvhimawf67KO9PytgPz3QoWye7Abapt0Yocw/JnzMiSNj/pRoIKyn8Jxypkv86zxKD4Q/zNTc0Ag==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "ipC4u1VojgEfoIZhtbS2Sx5IluJTP/Jf1hz3yGsxGBgSukYY/CquI6rAjxn5H58CZgVn36qcuPPtNMwZ0AUzMg==" + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "rVH43bcUyZiMn0SnCpVnvFpl4PFxT4GwmuVVLcT4JL0NtzuHY9ymKV+Llb5cjuJ+6+gEl4eixy2rE8nxOPcBSA==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "10.0.5", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "SQLitePCLRaw.core": "2.1.11" + } + }, + "Microsoft.Extensions.AmbientMetadata.Application": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "bovnONzrr/JIc+w343i857rJEb7cQH9UzEjbV5n67agWBEYICGQb8xiqYz5+GoFXp6mKEKLwYCQGttMU1p5yXQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.4", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Compliance.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "4WkknDbVrHNf+S6fwSt1OAXlGJ/G/QrtJlqx4aNzOLmeT3GRyxpGLZn+Q3UV+RMRAF6FfsijEZBg2ZAW8bTAkg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4" + } + }, + "Microsoft.Extensions.Configuration.CommandLine": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "or9fOLopMUTJOQVJ3bou4aD6PwvsiKf4kZC4EE5sRRKSkmh+wfk/LekJXRjAX88X+1JA9zHjDo+5fiQ7z3MY/A==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.FileExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.UserSecrets": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "fhdG6UV9lIp70QhNkVyaHciUVq25IPFkczheVJL9bIFvmnJ+Zghaie6dWkDbbVmxZlHl9gj3zTDxMxJs5zNhIA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Json": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.AutoActivation": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ksmUG2SFTcXzYdyoLOdeSM/qYLRGN6qbbSzYVkwMK9xsctfR1hYkUayeOpFCMd7L+QSlYX72mK9wxwdgQxyS4g==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vAJHd4yOpmKoK+jBuYV7a3y+Ab9U4ARCc29b6qvMy276RgJFw9LFs0DdsPqOL3ahwzyrX7tM+i4cCxU/RX0qAg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "1/hQmONMWxRTKXuN0pQShQN9QsqIRTS1G4fdmKW0O9phuVZjyzIROQD9Fbfwyn2t+yvP8SzjatGAPX4jDRfgHg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.4" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "REdt95QXHscGdtw/UUgyCW2lF9DJcAOJxmebKW2IkgUjuCAdMODIi2HNOWg5utW98nm8ekgV0Gjqs/sljwwqMw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": "10.0.5", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "NrIMTy7dpqxAvA6kHAYH8cXID/YgeNOy0OqFKpLtkPu5X4WS/basX91UszANzVrMNRAICJ2GOnGiRxJtsRyEQw==" + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" + }, + "Microsoft.Extensions.Http.Diagnostics": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "ybx2QcCWROCnUCbSj/IyHXn1c58brjjHzTTbueKgBl/qHsWk69mu25mjQ3oaMsO1I0+EcS6AhVuhIopL2q3IDw==", + "dependencies": { + "Microsoft.Extensions.Http": "10.0.4", + "Microsoft.Extensions.Telemetry": "10.4.0" + } + }, + "Microsoft.Extensions.Logging.Debug": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.EventLog": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "0ezhWYJS4/6KrqQel9JL+Tr4n+4EX2TF5EYiaysBWNNEM2c3Gtj1moD39esfgk8OHblSX+UFjtZ3z0c4i9tRvw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "System.Diagnostics.EventLog": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.EventSource": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "vN+aq1hBFXyYvY5Ow9WyeR66drKQxRZmas4lAjh6QWfryPkjTn1uLtX5AFIxyDaZj78v5TG2sELUyvrXpAPQQw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "10.0.4", + "contentHash": "2pufIFOgNl/yWTOoIC9XgBnO9VxgfAjdRCnVwpE2+ICfcroGnjuEAGzJ5lTdZeAe0HvA31vMBWXtcmGB7TOq3g==" + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" + }, + "Microsoft.Extensions.Resilience": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "41CCbJJPsDWU6NsmKfANHkfT/+KCBlZZqQ1eBoQhhW0xqGCiWmUlMdi2BoaM/GcwKHX5WiQL/IESROmgk0Owfw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics": "10.0.4", + "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "10.4.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0", + "Polly.Extensions": "8.4.2", + "Polly.RateLimiting": "8.4.2" + } + }, + "Microsoft.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "AbHleTzdpGPjA6RpOjKVHEYx7SoBRnJ2bwAbbPa3aGB7HiVwBmeTJhBGhtIBiuIW0VpKDS8x+bV5iWqpBRIf4w==", + "dependencies": { + "Microsoft.Extensions.AmbientMetadata.Application": "10.4.0", + "Microsoft.Extensions.DependencyInjection.AutoActivation": "10.4.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Telemetry.Abstractions": "10.4.0" + } + }, + "Microsoft.Extensions.Telemetry.Abstractions": { + "type": "Transitive", + "resolved": "10.4.0", + "contentHash": "3b2uVa4voJfLLg39BPCKQS0ZgnpEZFkKf7YmnMVlM5FQJYBPOuePIQdnEK1/Oxd+w3GscxGYuE7IMOXDwixZtQ==", + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "10.4.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.FeatureManagement": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "qxvGAv9WJHYfOpixWywJTa1WNTPy5MbQiv+O+UlE6E/LVofiM1+YRR6m41zsHIbAGm1S0PQ0QFuAsOw9DkoKsg==", + "dependencies": { + "Microsoft.Bcl.TimeProvider": "8.0.1", + "Microsoft.Extensions.Caching.Memory": "8.0.1", + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.2", + "Microsoft.Extensions.Logging": "8.0.1" + } + }, + "Microsoft.IdentityModel.Abstractions": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "6NrxQGcZg6IunkN8K2F0UVMavNpfCjbjjjON7PYcL8FwI8aULKUreiHsRX/yaA8j3XsTJnQKUYpoQk5gBjULZw==" + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "JbFZ3OVwtvqcqgBL0cIkhRYbIP7u9GIUYLOgbNqLWtBtZY8tGDpdGyXMzUVX0gVHq1ovuHsKZrkVv+ziHEnBHw==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.17.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "w1vjfri0BWqW7RkSZY3ZsqekNfIJJg5BQSFs2j+a+pCXOVrkezmJcn74pT3djwjXJh71577C6wJQgNc2UPz30w==", + "dependencies": { + "Microsoft.IdentityModel.Abstractions": "8.17.0" + } + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "8.17.0", + "contentHash": "teaW35URIV2x78Tzk+dVJiC4M62/9mQoSEoDjDGoEZmcQa3H2rE+XQpm9Tmdo9KK1Lcrnve4zoyLavl69kCFGg==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.IdentityModel.Logging": "8.17.0" + } + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.Testing.Extensions.Telemetry": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", + "dependencies": { + "Microsoft.ApplicationInsights": "2.23.0", + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Extensions.TrxReport.Abstractions": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.Testing.Platform": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" + }, + "Microsoft.Testing.Platform.MSBuild": { + "type": "Transitive", + "resolved": "1.9.1", + "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", + "dependencies": { + "Microsoft.Testing.Platform": "1.9.1" + } + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "18.4.0", + "contentHash": "4L6m2kS2pY5uJ9cpeRxzW22opr6ttScIRqsOpMDQpgENp/ZwxkkQCcmc6LRSURo2dFaaSW5KVflQZvroiJ7Wzg==" + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "18.4.0", + "contentHash": "gZsCHI+zOmZCcKZieIL4Jg14qKD2OGZOmX5DehuIk1EA9BN6Crm0+taXQNEuajOH1G9CCyBxw8VWR4t5tumcng==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "18.4.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.VisualStudio.SolutionPersistence": { + "type": "Transitive", + "resolved": "1.0.52", + "contentHash": "oNv2JtYXhpdJrX63nibx1JT3uCESOBQ1LAk7Dtz/sr0+laW0KRM6eKp4CZ3MHDR2siIkKsY8MmUkeP5DKkQQ5w==" + }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", + "dependencies": { + "System.CodeDom": "6.0.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + } + }, + "NetTopologySuite": { + "type": "Transitive", + "resolved": "2.6.0", + "contentHash": "1B1OTacTd4QtFyBeuIOcThwSSLUdRZU3bSFIwM8vk36XiZlBMi3K36u74e4OqwwHRHUuJC1PhbDx4hyI266X1Q==" + }, + "NetTopologySuite.IO.PostGis": { + "type": "Transitive", + "resolved": "2.1.0", + "contentHash": "3W8XTFz8iP6GQ5jDXK1/LANHiU+988k1kmmuPWNKcJLpmSg6CvFpbTpz+s4+LBzkAp64wHGOldSlkSuzYfrIKA==", + "dependencies": { + "NetTopologySuite": "[2.0.0, 3.0.0-A)" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.4", + "contentHash": "pdgNNMai3zv51W5aq268sujXUyx7SNdE2bj1wZcWjAQrKMFZV260lbqYop1d2GM67JI1huLRwxo9ZqnfF/lC6A==" + }, + "Npgsql.DependencyInjection": { + "type": "Transitive", + "resolved": "10.0.1", + "contentHash": "YHFa4vD27sNIfv6s5q8Zi1fLvKfmK1xcpMv0PUvXOxDFbRmuMRSHwpZTbPvsAlj97q1/o7DfyynLqfqrCm1VnA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Npgsql": "10.0.1" + } + }, + "Npgsql.NetTopologySuite": { + "type": "Transitive", + "resolved": "10.0.2", + "contentHash": "WNM5ZHATFj0dSMJ/4vlbgMWBgP0ElMwsGiHR0PwzwflNS+IXbX8xxNZyLp4veCLcr5tDCWwMh9Lw7nwUxWkzvg==", + "dependencies": { + "NetTopologySuite": "2.6.0", + "NetTopologySuite.IO.PostGIS": "2.1.0", + "Npgsql": "10.0.2" + } + }, + "Npgsql.OpenTelemetry": { + "type": "Transitive", + "resolved": "10.0.1", + "contentHash": "G9fEIBaHggZXWfDSDnKLc0XwKcbuU6i2eXp7zDqpgYxbhCmIN9fRgaSOGyyMNHSo/yY1IB4G4CjW5VO/SKRR0g==", + "dependencies": { + "Npgsql": "10.0.1", + "OpenTelemetry.API": "1.14.0" + } + }, + "OpenTelemetry": { + "type": "Transitive", + "resolved": "1.15.2", + "contentHash": "XwXZR69HnMBwwzvVg6ONRoPl5jeSFJkqOftHLcbSvl9DG6vY1j0OJut3cHu9Vmc5r2zgEDIQclAqpDtbhPNQZA==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Configuration": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "1.15.2" + } + }, + "OpenTelemetry.Api": { + "type": "Transitive", + "resolved": "1.15.2", + "contentHash": "7zeBVUVhVyVjzOUvEj8o+NkpEOKq77zjxH8akZBlN9LjuaSPUW/yV4EJrs393wxSLWC4eun1qwTN9NY21k5ixQ==" + }, + "OpenTelemetry.Api.ProviderBuilderExtensions": { + "type": "Transitive", + "resolved": "1.15.2", + "contentHash": "+UIz3GpBUrapW5IRnvPGdvVhhvW686lbIcNo/7ENOjB++djsdEsFxILTFYJ673cU0l8Da/OtV+IcE49OKJUM5w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "OpenTelemetry.Api": "1.15.2" + } + }, + "OpenTelemetry.PersistentStorage.Abstractions": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "QuBc6e7M4Skvbc+eTQGSmrcoho7lSkHLT5ngoSsVeeT8OXLpSUETNcuRPW8F5drTPTzzTKQ98C5AhKO/pjpTJg==" + }, + "OpenTelemetry.PersistentStorage.FileSystem": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "ys0l9vL0/wOV9p/iuyDeemjX+d8iH4yjaYA1IcmyQUw0xsxx0I3hQm7tN3FnuRPsmPtrohiLtp31hO1BcrhQ+A==", + "dependencies": { + "OpenTelemetry.PersistentStorage.Abstractions": "1.0.2" + } + }, + "Pipelines.Sockets.Unofficial": { + "type": "Transitive", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==" + }, + "Polly.Core": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" + }, + "Polly.Extensions": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Polly.Core": "8.4.2" + } + }, + "Polly.RateLimiting": { + "type": "Transitive", + "resolved": "8.4.2", + "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", + "dependencies": { + "Polly.Core": "8.4.2", + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "Microsoft.Extensions.Logging.Abstractions": "10.0.0", + "Serilog": "4.3.0", + "Serilog.Extensions.Logging": "10.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "10.0.0", + "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", + "dependencies": { + "Microsoft.Extensions.Logging": "10.0.0", + "Serilog": "4.2.0" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", + "dependencies": { + "Serilog": "4.2.0" + } + }, + "SharpZipLib": { + "type": "Transitive", + "resolved": "1.4.2", + "contentHash": "yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==", + "dependencies": { + "SQLitePCLRaw.lib.e_sqlite3": "2.1.11", + "SQLitePCLRaw.provider.e_sqlite3": "2.1.11" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA==" + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Ev2ytaXiOlWZ4b3R67GZBsemTINslLD1DCJr2xiacpn4tbapu0Q4dHEzSvZSMnVWeE5nlObU3VZN2p81q3XOYQ==" + }, + "SQLitePCLRaw.provider.e_sqlite3": { + "type": "Transitive", + "resolved": "2.1.11", + "contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==", + "dependencies": { + "SQLitePCLRaw.core": "2.1.11" + } + }, + "SSH.NET": { + "type": "Transitive", + "resolved": "2025.1.0", + "contentHash": "jrnbtf0ItVaXAe6jE8X/kSLa6uC+0C+7W1vepcnRQB/rD88qy4IxG7Lf1FIbWmkoc4iVXv0pKrz+Wc6J4ngmHw==", + "dependencies": { + "BouncyCastle.Cryptography": "2.6.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3" + } + }, + "StackExchange.Redis": { + "type": "Transitive", + "resolved": "2.7.27", + "contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" + } + }, + "Swashbuckle.AspNetCore.Swagger": { + "type": "Transitive", + "resolved": "10.1.7", + "contentHash": "EjLibt/d/QuRv170GoihTbcPUpgzSFm2WKHhnGJFZQ03JYzfuitsM79azaAR8NBwRunU7yScSX6HRE5JUlrEMQ==", + "dependencies": { + "Microsoft.OpenApi": "2.4.1" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "Transitive", + "resolved": "10.1.7", + "contentHash": "iJo3ODyUb/M8Vm8AH1r9y9iAba0w95xsCn3zFVl96ISRHbTDWxi+l7oFVCZqUEdjd97B8VMDPnMliWAdomR8uw==" + }, + "System.ClientModel": { + "type": "Transitive", + "resolved": "1.8.0", + "contentHash": "AqRzhn0v29GGGLj/Z6gKq4lGNtvPHT4nHdG5PDJh9IfVjv/nYUVmX11hwwws1vDFeIAzrvmn0dPu8IjLtu6fAw==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", + "System.Memory.Data": "8.0.1" + } + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" + }, + "System.Composition": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "3Djj70fFTraOarSKmRnmRy/zm4YurICm+kiCtI0dYRqGJnLX6nJ+G3WYuFJ173cAPax/gh96REcbNiVqcrypFQ==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Convention": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0", + "System.Composition.TypedParts": "9.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "iri00l/zIX9g4lHMY+Nz0qV1n40+jFYAmgsaiNn16xvt2RDwlqByNG4wgblagnDYxm3YSQQ0jLlC/7Xlk9CzyA==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "+vuqVP6xpi582XIjJi6OCsIxuoTZfR0M7WWufk3uGDeCl3wGW6KnpylUJ3iiXdPByPE0vR5TjJgR6hDLez4FQg==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "OFqSeFeJYr7kHxDfaViGM1ymk7d4JxK//VSoNF9Ux0gpqkLsauDZpu89kTHHNdCWfSljbFcvAafGyBoY094btQ==", + "dependencies": { + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "w1HOlQY1zsOWYussjFGZCEYF2UZXgvoYnS94NIu2CBnAGMbXFAX8PY8c92KwUItPmowal68jnVLBCzdrWLeEKA==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "9.0.0", + "contentHash": "aRZlojCCGEHDKqh43jaDgaVpYETsgd7Nx4g1zwLKMtv4iTo0627715ajEFNpEEBTgLmvZuv8K0EVxc3sM4NWJA==", + "dependencies": { + "System.Composition.AttributedModel": "9.0.0", + "System.Composition.Hosting": "9.0.0", + "System.Composition.Runtime": "9.0.0" + } + }, + "System.Diagnostics.EventLog": { + "type": "Transitive", + "resolved": "10.0.5", + "contentHash": "wugvy+pBVzjQEnRs9wMTWwoaeNFX3hsaHeVHFDIvJSWXp7wfmNWu3mxAwBIE6pyW+g6+rHa1Of5fTzb0QVqUTA==" + }, + "System.Memory.Data": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "BVYuec3jV23EMRDeR7Dr1/qhx7369dZzJ9IWy2xylvb4YfXsrUxspWc4UWYid/tj4zZK58uGZqn2WQiaDMhmAg==" + }, + "System.Threading.RateLimiting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" + }, + "Testcontainers": { + "type": "Transitive", + "resolved": "4.11.0", + "contentHash": "9pBNaK9Ra3GVnr5h6gaDJOBH0txA5G3Juho5WANPuyu38l5xyr2lCvf11oA5/uSd+Lh4Wtng34nKp3nMiea02g==", + "dependencies": { + "Docker.DotNet.Enhanced": "3.131.1", + "Docker.DotNet.Enhanced.X509": "3.131.1", + "Microsoft.Extensions.Logging.Abstractions": "8.0.3", + "SSH.NET": "2025.1.0", + "SharpZipLib": "1.4.2" + } + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "1.27.0", + "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" + }, + "xunit.v3.assert": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" + }, + "xunit.v3.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "6.0.0" + } + }, + "xunit.v3.core.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", + "dependencies": { + "Microsoft.Testing.Extensions.Telemetry": "1.9.1", + "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", + "Microsoft.Testing.Platform": "1.9.1", + "Microsoft.Testing.Platform.MSBuild": "1.9.1", + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.inproc.console": "[3.2.2]" + } + }, + "xunit.v3.extensibility.core": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", + "dependencies": { + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.mtp-v1": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", + "dependencies": { + "xunit.analyzers": "1.27.0", + "xunit.v3.assert": "[3.2.2]", + "xunit.v3.core.mtp-v1": "[3.2.2]" + } + }, + "xunit.v3.runner.common": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", + "dependencies": { + "Microsoft.Win32.Registry": "[5.0.0]", + "xunit.v3.common": "[3.2.2]" + } + }, + "xunit.v3.runner.inproc.console": { + "type": "Transitive", + "resolved": "3.2.2", + "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", + "dependencies": { + "xunit.v3.extensibility.core": "[3.2.2]", + "xunit.v3.runner.common": "[3.2.2]" + } + }, + "meajudaai.apiservice": { + "type": "Project", + "dependencies": { + "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )", + "AspNetCore.HealthChecks.Redis": "[9.0.0, )", + "AspNetCore.HealthChecks.UI.Client": "[9.0.0, )", + "MeAjudaAi.Contracts": "[1.0.0, )", + "MeAjudaAi.Modules.Communications.API": "[1.0.0, )", + "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", + "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", + "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", + "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", + "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", + "MeAjudaAi.Modules.Users.API": "[1.0.0, )", + "MeAjudaAi.ServiceDefaults": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Sinks.Seq": "[9.0.0, )", + "Swashbuckle.AspNetCore": "[10.1.7, )", + "Swashbuckle.AspNetCore.Annotations": "[10.1.7, )" + } + }, + "meajudaai.contracts": { + "type": "Project", + "dependencies": { + "FluentValidation": "[12.1.1, )" + } + }, + "meajudaai.modules.communications.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Communications.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Communications.Infrastructure": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.Http.Abstractions": "[2.3.9, )", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Abstractions": "[10.0.5, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.5, )" + } + }, + "meajudaai.modules.communications.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Contracts": "[1.0.0, )", + "MeAjudaAi.Modules.Communications.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.communications.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.communications.infrastructure": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "MeAjudaAi.Modules.Communications.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.documents.api": { + "type": "Project", + "dependencies": { + "Asp.Versioning.Http": "[8.1.1, )", + "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )", + "MeAjudaAi.Modules.Documents.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Documents.Infrastructure": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )" + } + }, + "meajudaai.modules.documents.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Documents.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.documents.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.documents.infrastructure": { + "type": "Project", + "dependencies": { + "Azure.AI.DocumentIntelligence": "[1.0.0, )", + "Azure.Storage.Blobs": "[12.27.0, )", + "EFCore.NamingConventions": "[10.0.1, )", + "MeAjudaAi.Modules.Documents.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Documents.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, + "meajudaai.modules.locations.api": { + "type": "Project", + "dependencies": { + "Asp.Versioning.Http": "[8.1.1, )", + "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )", + "MeAjudaAi.Modules.Locations.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Locations.Infrastructure": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )" + } + }, + "meajudaai.modules.locations.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Locations.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.locations.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.locations.infrastructure": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "MeAjudaAi.Modules.Locations.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Locations.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.providers.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Providers.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Providers.Infrastructure": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.Http.Abstractions": "[2.3.9, )", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Abstractions": "[10.0.5, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.5, )" + } + }, + "meajudaai.modules.providers.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Providers.Domain": "[1.0.0, )", + "MeAjudaAi.Modules.ServiceCatalogs.Application": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.providers.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.providers.infrastructure": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "MeAjudaAi.Modules.Providers.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Providers.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, + "meajudaai.modules.searchproviders.api": { + "type": "Project", + "dependencies": { + "Asp.Versioning.Http": "[8.1.1, )", + "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )", + "MeAjudaAi.Modules.SearchProviders.Application": "[1.0.0, )", + "MeAjudaAi.Modules.SearchProviders.Infrastructure": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )" + } + }, + "meajudaai.modules.searchproviders.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.SearchProviders.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.searchproviders.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.searchproviders.infrastructure": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "MeAjudaAi.Modules.SearchProviders.Application": "[1.0.0, )", + "MeAjudaAi.Modules.SearchProviders.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite": "[10.0.1, )" + } + }, + "meajudaai.modules.servicecatalogs.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.ServiceCatalogs.Application": "[1.0.0, )", + "MeAjudaAi.Modules.ServiceCatalogs.Infrastructure": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.Http.Abstractions": "[2.3.9, )", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Abstractions": "[10.0.5, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.5, )" + } + }, + "meajudaai.modules.servicecatalogs.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.ServiceCatalogs.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.servicecatalogs.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.servicecatalogs.infrastructure": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "MeAjudaAi.Modules.ServiceCatalogs.Application": "[1.0.0, )", + "MeAjudaAi.Modules.ServiceCatalogs.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.users.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Users.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Users.Infrastructure": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.Http.Abstractions": "[2.3.9, )", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.5, )", + "Microsoft.Extensions.Configuration.Abstractions": "[10.0.5, )", + "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.5, )" + } + }, + "meajudaai.modules.users.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Users.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.users.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.users.infrastructure": { + "type": "Project", + "dependencies": { + "EFCore.NamingConventions": "[10.0.1, )", + "MeAjudaAi.Modules.Users.Application": "[1.0.0, )", + "MeAjudaAi.Modules.Users.Domain": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "System.IdentityModel.Tokens.Jwt": "[8.17.0, )" + } + }, + "meajudaai.servicedefaults": { + "type": "Project", + "dependencies": { + "Aspire.Npgsql": "[13.2.2, )", + "Azure.Monitor.OpenTelemetry.AspNetCore": "[1.4.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.Extensions.Http.Resilience": "[10.4.0, )", + "Microsoft.FeatureManagement.AspNetCore": "[4.4.0, )", + "OpenTelemetry.Exporter.Console": "[1.15.2, )", + "OpenTelemetry.Exporter.OpenTelemetryProtocol": "[1.15.2, )", + "OpenTelemetry.Extensions.Hosting": "[1.15.2, )", + "OpenTelemetry.Instrumentation.AspNetCore": "[1.15.1, )", + "OpenTelemetry.Instrumentation.EntityFrameworkCore": "[1.14.0-beta.2, )", + "OpenTelemetry.Instrumentation.Http": "[1.15.0, )", + "OpenTelemetry.Instrumentation.Runtime": "[1.15.0, )" + } + }, + "meajudaai.shared": { + "type": "Project", + "dependencies": { + "Asp.Versioning.Mvc": "[8.1.1, )", + "Asp.Versioning.Mvc.ApiExplorer": "[8.1.1, )", + "AspNetCore.HealthChecks.Npgsql": "[9.0.0, )", + "AspNetCore.HealthChecks.Redis": "[9.0.0, )", + "Dapper": "[2.1.72, )", + "EFCore.NamingConventions": "[10.0.1, )", + "FluentValidation": "[12.1.1, )", + "FluentValidation.DependencyInjectionExtensions": "[12.1.1, )", + "Hangfire.AspNetCore": "[1.8.23, )", + "Hangfire.Core": "[1.8.23, )", + "Hangfire.PostgreSql": "[1.21.1, )", + "MeAjudaAi.Contracts": "[1.0.0, )", + "Microsoft.AspNetCore.OpenApi": "[10.0.5, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Microsoft.Extensions.Caching.Hybrid": "[10.4.0, )", + "Microsoft.Extensions.Caching.StackExchangeRedis": "[10.0.5, )", + "Microsoft.FeatureManagement.AspNetCore": "[4.4.0, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "RabbitMQ.Client": "[7.2.1, )", + "Rebus": "[8.9.0, )", + "Rebus.RabbitMq": "[10.1.1, )", + "Rebus.ServiceProvider": "[10.7.2, )", + "Scrutor": "[7.0.0, )", + "Serilog": "[4.3.1, )", + "Serilog.AspNetCore": "[10.0.0, )", + "Serilog.Enrichers.Environment": "[3.0.1, )", + "Serilog.Enrichers.Process": "[3.0.0, )", + "Serilog.Enrichers.Thread": "[4.0.0, )", + "Serilog.Settings.Configuration": "[10.0.0, )", + "Serilog.Sinks.Console": "[6.1.1, )", + "Serilog.Sinks.Seq": "[9.0.0, )" + } + }, + "meajudaai.shared.tests": { + "type": "Project", + "dependencies": { + "AutoFixture": "[4.18.1, )", + "AutoFixture.AutoMoq": "[4.18.1, )", + "Bogus": "[35.6.5, )", + "Dapper": "[2.1.72, )", + "FluentAssertions": "[8.9.0, )", + "Hangfire.InMemory": "[1.0.0, )", + "MeAjudaAi.ApiService": "[1.0.0, )", + "MeAjudaAi.Shared": "[1.0.0, )", + "Microsoft.AspNetCore.Mvc.Testing": "[10.0.5, )", + "Microsoft.Extensions.Hosting": "[10.0.5, )", + "Microsoft.Extensions.Logging.Abstractions": "[10.0.5, )", + "Microsoft.Extensions.TimeProvider.Testing": "[10.4.0, )", + "Microsoft.NET.Test.Sdk": "[18.4.0, )", + "Moq": "[4.20.72, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )", + "Respawn": "[7.0.0, )", + "Scrutor": "[7.0.0, )", + "Testcontainers.Azurite": "[4.11.0, )", + "Testcontainers.PostgreSql": "[4.11.0, )", + "Testcontainers.RabbitMq": "[4.11.0, )", + "xunit.v3": "[3.2.2, )" + } + }, + "Asp.Versioning.Http": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "1D/Mzq1MSUSQe1eCY6GeEsu+tIlpzoWZxZkhlcw/uvtVoTrwO+BMl0fSj/XX8oZN1DutWTZqHDHgyDRq+IeSdQ==", + "dependencies": { + "Asp.Versioning.Abstractions": "8.1.0" + } + }, + "Asp.Versioning.Mvc": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "mkJv6eAdlbHbqTdrUcfEYFoZuGL6HoR7O+Lfsvivixp7N5BNhfCFPPOwsBzdIiH1qzdJXyJf+C+DZ08j27PMPg==", + "dependencies": { + "Asp.Versioning.Http": "8.1.1" + } + }, + "Asp.Versioning.Mvc.ApiExplorer": { + "type": "CentralTransitive", + "requested": "[8.1.1, )", + "resolved": "8.1.1", + "contentHash": "u8PrP6CjmurgIh0EDfg8Gc/GdpDHgkbv8OLKdxQcWb5W4sp0i9BcdbQlLvRcATjNIJUyr7ZmqXjmdsFfqnuy0g==", + "dependencies": { + "Asp.Versioning.Mvc": "8.1.1" + } + }, + "Aspire.Npgsql": { + "type": "CentralTransitive", + "requested": "[13.2.2, )", + "resolved": "13.2.2", + "contentHash": "nEYgziWN7hksgEQEWy24JypcMCU8gKYcIIyPL05JfdXxUWuPRLotH/KOeuHevAjSEOYkL3dtGakBkJAuPobGmA==", + "dependencies": { + "AspNetCore.HealthChecks.NpgSql": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.HealthChecks": "10.0.5", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5", + "Npgsql.DependencyInjection": "10.0.1", + "Npgsql.OpenTelemetry": "10.0.1", + "OpenTelemetry.Extensions.Hosting": "1.15.0" + } + }, + "AspNetCore.HealthChecks.NpgSql": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "npc58/AD5zuVxERdhCl2Kb7WnL37mwX42SJcXIwvmEig0/dugOLg3SIwtfvvh3TnvTwR/sk5LYNkkPaBdks61A==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "Npgsql": "8.0.3" + } + }, + "AspNetCore.HealthChecks.Redis": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "yNH0h8GLRbAf+PU5HNVLZ5hNeyq9mDVmRKO9xuZsme/znUYoBJlQvI0gq45gaZNlLncCHkMhR4o90MuT+gxxPw==", + "dependencies": { + "Microsoft.Extensions.Diagnostics.HealthChecks": "8.0.11", + "StackExchange.Redis": "2.7.4" + } + }, + "AspNetCore.HealthChecks.UI.Client": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "1Ub3Wvvbz7CMuFNWgLEc9qqQibiMoovDML/WHrwr5J83RPgtI20giCR92s/ipLgu7IIuqw+W/y7WpIeHqAICxg==", + "dependencies": { + "AspNetCore.HealthChecks.UI.Core": "9.0.0" + } + }, + "Azure.AI.DocumentIntelligence": { + "type": "CentralTransitive", + "requested": "[1.0.0, )", + "resolved": "1.0.0", + "contentHash": "RSpMmlRY5vvGy2TrAk4djJTqOsdHUunvhcSoSN+FJtexqZh6RFn+a2ylehIA/N+HV2IK0i+XK4VG3rDa8h2tsA==", + "dependencies": { + "Azure.Core": "1.44.1", + "System.ClientModel": "1.2.1" + } + }, + "Azure.Monitor.OpenTelemetry.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.4.0, )", + "resolved": "1.4.0", + "contentHash": "Zs9wBCBLkm/8Fz97GfRtbuhgd4yPlM8RKxaL6owlW2KcmO8kMqjNK/2riR5DUF5ck8KloFsUg+cuGTDmIHlqww==", + "dependencies": { + "Azure.Core": "1.50.0", + "Azure.Monitor.OpenTelemetry.Exporter": "1.5.0", + "OpenTelemetry.Extensions.Hosting": "1.14.0", + "OpenTelemetry.Instrumentation.AspNetCore": "1.14.0", + "OpenTelemetry.Instrumentation.Http": "1.14.0" + } + }, + "Azure.Storage.Blobs": { + "type": "CentralTransitive", + "requested": "[12.27.0, )", + "resolved": "12.27.0", + "contentHash": "zI5rg1tTtnA8T2g2/21l+1iIUdDjpEQQ0FI1BabJVEQJ1JUyTQKrc41eNabAHs0SBHprl6pu/6OqIMK9Ve+4tQ==", + "dependencies": { + "Azure.Core": "1.50.0", + "Azure.Storage.Common": "12.26.0" + } + }, + "Azure.Storage.Common": { + "type": "CentralTransitive", + "requested": "[12.26.0, )", + "resolved": "12.26.0", + "contentHash": "XaT6CDcSshZb7KaCTwc6m4EouZbLBg7ciOEpsJSdJCvkNsZJQCvPKw7V5TtXno19AA1NpwtsZriYque8mzbQVg==", + "dependencies": { + "Azure.Core": "1.50.0", + "System.IO.Hashing": "10.0.1" + } + }, + "Dapper": { + "type": "CentralTransitive", + "requested": "[2.1.72, )", + "resolved": "2.1.72", + "contentHash": "ns4mGqQd9a/MhP8m6w556vVlZIa0/MfUu03zrxjZC/jlr1uVCsUac8bkdB+Fs98Llbd56rRSo1eZH5VVmeGZyw==" + }, + "EFCore.NamingConventions": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Xs5k8XfNKPkkQSkGmZkmDI1je0prLTdxse+s8PgTFZxyBrlrTLzTBUTVJtQKSsbvu4y+luAv8DdtO5SALJE++A==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.1, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.1, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.1" + } + }, + "FluentValidation": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" + }, + "FluentValidation.DependencyInjectionExtensions": { + "type": "CentralTransitive", + "requested": "[12.1.1, )", + "resolved": "12.1.1", + "contentHash": "D0VXh4dtjjX2aQizuaa0g6R8X3U1JaVqJPfGCvLwZX9t/O2h7tkpbitbadQMfwcgSPdDbI2vDxuwRMv/Uf9dHA==", + "dependencies": { + "FluentValidation": "12.1.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.1.0" + } + }, + "Hangfire.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "TXpOl7kX4xXq5bLEqqWCpt9zh3TaouDwtb3GDtzGHX5uSC2RaAqZzn2swevivx3Uki16slXIigiPtgr4TPKpsg==", + "dependencies": { + "Hangfire.NetCore": "[1.8.23]" + } + }, + "Hangfire.Core": { + "type": "CentralTransitive", + "requested": "[1.8.23, )", + "resolved": "1.8.23", + "contentHash": "YCOTtF3NNOQI83PlfjeNDDBkofJDfdET2CwhfQsiVBwmsU6lP19QW9NVTIH9epl+MnOsyFC2G1RnlPSGV8F1FQ==", + "dependencies": { + "Newtonsoft.Json": "11.0.1" + } + }, + "Hangfire.InMemory": { + "type": "CentralTransitive", + "requested": "[1.0.0, )", + "resolved": "1.0.0", + "contentHash": "56H71lfcqn5sN/8Bjj9hOLGTG5HIERLRuMsRJTFpw0Tsq5ck5OUkNvtUw92s7bwD3PRKOo4PkDGqNs9KugaqoQ==", + "dependencies": { + "Hangfire.Core": "1.8.0" + } + }, + "Hangfire.PostgreSql": { + "type": "CentralTransitive", + "requested": "[1.21.1, )", + "resolved": "1.21.1", + "contentHash": "hFNZAxv+1p72/XCZdImnH6ovCzZ2DKAMTOI8CReT0P3yw/k0b0YJP2teA18agNH1ZYInPzhtxGk8hx5n2cxbbQ==", + "dependencies": { + "Dapper": "2.0.123", + "Dapper.AOT": "1.0.48", + "Hangfire.Core": "1.8.0", + "Npgsql": "6.0.11" + } + }, + "Microsoft.AspNetCore.Authentication.JwtBearer": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", + "dependencies": { + "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "CentralTransitive", + "requested": "[2.3.9, )", + "resolved": "2.3.9", + "contentHash": "ULScB/0S9+qvf+yahjR+oQUp0GrvoDHJ9XS5gTqSjLjbjUDnHaJ1s8wo3RJMpaDfb1bawX4OgQM+YmvCUveR4Q==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "2.3.0" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "vTcxIfOPyfFbYk1g8YcXJfkMnlEWVkSnnjxcZLy60zgwiHMRf2SnZR+9E4HlpwKxgE3yfKMOti8J6WfKuKsw6w==", + "dependencies": { + "Microsoft.OpenApi": "2.0.0" + } + }, + "Microsoft.AspNetCore.TestHost": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==" + }, + "Microsoft.Build.Framework": { + "type": "CentralTransitive", + "requested": "[18.0.2, )", + "resolved": "18.0.2", + "contentHash": "sOSb+0J4G/jCBW/YqmRuL0eOMXgfw1KQLdC9TkbvfA5xs7uNm+PBQXJCOzSJGXtZcZrtXozcwxPmUiRUbmd7FA==" + }, + "Microsoft.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9tNBmK3EpYVGRQLiqP+bqK2m+TD0Gv//4vCzR7ZOgl4FWzCFyOpYdIVka13M4kcBdPdSJcs3wbHr3rmzOqbIMA==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "10.0.5", + "Microsoft.EntityFrameworkCore.Analyzers": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "gm6f0cC2w/2tcd4GeZJqEMruTercpIJfO5sSAFLtqTqblDBHgAFk70xwshUIUVX4I6sZwdEUSd1YxoKFk1AL0w==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Build.Framework": "18.0.2", + "Microsoft.CodeAnalysis.CSharp": "5.0.0", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "5.0.0", + "Microsoft.CodeAnalysis.Workspaces.MSBuild": "5.0.0", + "Microsoft.EntityFrameworkCore.Relational": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyModel": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Mono.TextTemplating": "3.0.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "uxmFjZEAB/KbsgWFSS4lLqkEHCfXxB2x0UcbiO4e5fCRpFFeTMSx/me6009nYJLu5IKlDwO1POh++P6RilFTDw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "10.0.5", + "Microsoft.Extensions.Caching.Memory": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "Lx8zu/RWUAuPuDfM4S3AeXGNu50G/oDMw2MIWRsZPm+giT0iChOGln6ezM5CEuI7SS2IKOPmAp+ry4pMBv/r/w==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Caching.Hybrid": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "4V+aMLQeU/p4VcIWIcvGro0L6HynmL2TrelL04Ce1iotP6T5+kjxuZQvl6P1ObSXIRPCbVXtQSt1NxK0fRIuag==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.4", + "Microsoft.Extensions.Caching.Memory": "10.0.4", + "Microsoft.Extensions.Logging.Abstractions": "10.0.4", + "Microsoft.Extensions.Options": "10.0.4" + } + }, + "Microsoft.Extensions.Caching.StackExchangeRedis": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "zXb143/TpEKOLQuWGw2CkJgb9F4XXh2XbevMvppzsIHr1/pjML0zjc+vzXcpCV8YUwpW5NIaScZhzFSm621B3Q==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "StackExchange.Redis": "2.7.27" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", + "dependencies": { + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.EnvironmentVariables": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Configuration.Json": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "xA4kkL+QS6KCAOKz/O0oquHs44Ob8J7zpBCNt3wjkBWDg5aCqfwG8rWWLsg5V86AM0sB849g9JjPjIdksTCIKg==" + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "/nYGrpa9/0BZofrVpBbbj+Ns8ZesiPE0V/KxsuHgDgHQopIzN54nRaQGSuvPw16/kI9sW1Zox5yyAPqvf0Jz6A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "8i7e5IBdiKLNqt/+ciWrS8U95Rv5DClaaj7ulkZbimnCi4uREWd+lXzkp3joofFuIPOlAzV4AckxLTIELv2jdg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.Configuration.CommandLine": "10.0.5", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.5", + "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", + "Microsoft.Extensions.Configuration.Json": "10.0.5", + "Microsoft.Extensions.Configuration.UserSecrets": "10.0.5", + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Physical": "10.0.5", + "Microsoft.Extensions.Hosting.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Configuration": "10.0.5", + "Microsoft.Extensions.Logging.Console": "10.0.5", + "Microsoft.Extensions.Logging.Debug": "10.0.5", + "Microsoft.Extensions.Logging.EventLog": "10.0.5", + "Microsoft.Extensions.Logging.EventSource": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+Wb7KAMVZTomwJkQrjuPTe5KBzGod7N8XeG+ScxRlkPOB4sZLG4ccVwjV4Phk5BCJt7uIMnGHVoN6ZMVploX+g==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.5", + "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Http": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "AiFvHYM8nP0wPC7bGPI3NHQlSYSLqjjT7DMJUuuxhd+7pz3O89iu2gdQfgACy5DxsXENiok5i1bMacJL7KR8jA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Diagnostics": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Http.Resilience": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "HbkUsPUC7vLy2TaDbdA9aooW64n9yX4sUppRuiJ1cOzzU1FUW+MVEotm6kYVq6AuUI9xwFSBhRFzA03blmk3VA==", + "dependencies": { + "Microsoft.Extensions.Http.Diagnostics": "10.4.0", + "Microsoft.Extensions.ObjectPool": "10.0.4", + "Microsoft.Extensions.Resilience": "10.4.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.5", + "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", + "Microsoft.Extensions.Configuration.Binder": "10.0.5", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5", + "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging": "10.0.5", + "Microsoft.Extensions.Logging.Abstractions": "10.0.5", + "Microsoft.Extensions.Logging.Configuration": "10.0.5", + "Microsoft.Extensions.Options": "10.0.5" + } + }, + "Microsoft.Extensions.Options": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", + "Microsoft.Extensions.Primitives": "10.0.5" + } + }, + "Microsoft.Extensions.TimeProvider.Testing": { + "type": "CentralTransitive", + "requested": "[10.4.0, )", + "resolved": "10.4.0", + "contentHash": "uJ8n9WUEzux9I2CjZh7imGBgZadfwhAKlxuBq7GsNGL8FJF81aHXAYaRMnwW+9EvRFQNytu7xo1ffeuuTncAzg==" + }, + "Microsoft.FeatureManagement.AspNetCore": { + "type": "CentralTransitive", + "requested": "[4.4.0, )", + "resolved": "4.4.0", + "contentHash": "jE5KxeEDUnUsx1w9uOFV5cOfM4E2mg7kf07328xO1x4JdoS0jwkD55nMjTlUPu90ynYX1oCBGC+FHK6ZoqRpxA==", + "dependencies": { + "Microsoft.FeatureManagement": "4.4.0" + } + }, + "Microsoft.IdentityModel.Protocols": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "UFrU7d46UTsPQTa2HIEIpB9H1uJe1BW9FLw5uhEJ2ZuKdur8bcUA/bO5caq5dlBt5gNJeRIB3QQXYNs5fCQCZA==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "8.16.0" + } + }, + "Microsoft.IdentityModel.Protocols.OpenIdConnect": { + "type": "CentralTransitive", + "requested": "[8.16.0, )", + "resolved": "8.16.0", + "contentHash": "h4yVXyJsEBBX5lg2G5ftMsi5JzcNEGAzrNphA6DQ6eOd8P0s+cDCOyPwVTYLePZvJL5unbPvYIvzrbTXzFjXnQ==", + "dependencies": { + "Microsoft.IdentityModel.Protocols": "8.16.0", + "System.IdentityModel.Tokens.Jwt": "8.16.0" + } + }, + "Microsoft.OpenApi": { + "type": "CentralTransitive", + "requested": "[2.7.0, )", + "resolved": "2.7.0", + "contentHash": "b9xmpnmjq6p+HqF3uWG7u7/PlB38t/UB5UtXdi6xEAP9ZJGKHneYyjMGzBflB1rpLxYEcU6KRme+cz5wNPlxqA==" + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "P6EwH0Q4xkaA264iNZDqCPhWt8pscfUGxXazDQg4noBfqjoOlk4hKWfvBjF9ZX3R/9JybRmmJfmxr2iBMj0EpA==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[10.0.4, 11.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[10.0.4, 11.0.0)", + "Npgsql": "10.0.2" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "Fcx8k5cSF7cGeXMeguLwyV+wTcKbmhojCse2WBXfHlEQNXILpI1My7H6rvPT8n3fI4TxZiYq/LWO2vVGBD3FHA==", + "dependencies": { + "Npgsql.EntityFrameworkCore.PostgreSQL": "10.0.1", + "Npgsql.NetTopologySuite": "10.0.2" + } + }, + "OpenTelemetry.Exporter.Console": { + "type": "CentralTransitive", + "requested": "[1.15.2, )", + "resolved": "1.15.2", + "contentHash": "25RBbplOlkSHyKF9qVAA5DhGm47v0ghI++75ZgrUqOQKoEJ5GLOww77q625Z0WEZoJE7XyrCRBXIE7ggKoH41g==", + "dependencies": { + "OpenTelemetry": "1.15.2" + } + }, + "OpenTelemetry.Exporter.OpenTelemetryProtocol": { + "type": "CentralTransitive", + "requested": "[1.15.2, )", + "resolved": "1.15.2", + "contentHash": "qHAkKEGQ0REcwFP/gmldTbo3NxgG+0R6od5N7ndyXuJYqWRaWF38bko0KQkK57skSapMUXS8twQbHNMEEYCKLg==", + "dependencies": { + "OpenTelemetry": "1.15.2" + } + }, + "OpenTelemetry.Extensions.Hosting": { + "type": "CentralTransitive", + "requested": "[1.15.2, )", + "resolved": "1.15.2", + "contentHash": "T2nEP/lABOab8w8espx5biYGghw8errNlhhMLYmoXEhGT6EGB4CNhZInJPC+tOvlGUGFe54NM3TdSATxebwJ8g==", + "dependencies": { + "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", + "OpenTelemetry": "1.15.2" + } + }, + "OpenTelemetry.Instrumentation.AspNetCore": { + "type": "CentralTransitive", + "requested": "[1.15.1, )", + "resolved": "1.15.1", + "contentHash": "wXaZTu6LHY8xcbRd6ClcrtjHqGVoGYCcArXEZA3iUjUcYSVYwDGyPU0PdkwTfylxv8JeCCVDQhVb0fT7xBJjGA==", + "dependencies": { + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.EntityFrameworkCore": { + "type": "CentralTransitive", + "requested": "[1.14.0-beta.2, )", + "resolved": "1.14.0-beta.2", + "contentHash": "XsxsKgMuwi84TWkPN98H8FLOO/yW8vWIo/lxXQ8kWXastTI58+A4nmlFderFPmpLc+tvyhOGjHDlTK/AXWWOpQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.14.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Http": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "uToc7bUp8IEdb0ny9mKsL6FrrYelINPzxxiSShJgOf4XmQc4Azww6S5RjRj24YhsOn2a1MABOrxfVTZXtDk4Eg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "10.0.0", + "Microsoft.Extensions.Options": "10.0.0", + "OpenTelemetry.Api.ProviderBuilderExtensions": "[1.15.0, 2.0.0)" + } + }, + "OpenTelemetry.Instrumentation.Runtime": { + "type": "CentralTransitive", + "requested": "[1.15.0, )", + "resolved": "1.15.0", + "contentHash": "OOvpqR/j2Pb6+tWhHNODIbSJ53Or/MDtTiXEyrsWI02K2lLAgvBFcxUOrHggS/8015cYR3AdSaXv6NZrkz5yQA==", + "dependencies": { + "OpenTelemetry.Api": "[1.15.0, 2.0.0)" + } + }, + "RabbitMQ.Client": { + "type": "CentralTransitive", + "requested": "[7.2.1, )", + "resolved": "7.2.1", + "contentHash": "YKXEfg9fVQiTKgZlvIhAfPSFaamEgi8DsQmisCH0IAsU4FYLrtoguDrDj6JtJVGUt40QPnBLRH6fTQcAC4qsOg==", + "dependencies": { + "System.Threading.RateLimiting": "8.0.0" + } + }, + "Rebus": { + "type": "CentralTransitive", + "requested": "[8.9.0, )", + "resolved": "8.9.0", + "contentHash": "UaPGZuXIL4J5GUDA05JzEEzuPMEXY0CoF92nC6bsFBPvwoYPQ0uKyH2vKqdV80CW7cjbwBgDlEZ7R9hO9b59XA==", + "dependencies": { + "Newtonsoft.Json": "13.0.4" + } + }, + "Rebus.RabbitMq": { + "type": "CentralTransitive", + "requested": "[10.1.1, )", + "resolved": "10.1.1", + "contentHash": "66pUp4hfaYWfQEDOiVcuZQnPF4XFHyJ5KCfwCm18e3Dnr936Iog48KrN8Mp8QyRQ2tiNpzdjSATQLKEZpSk11A==", + "dependencies": { + "RabbitMq.Client": "7.1.2", + "rebus": "8.9.0" + } + }, + "Rebus.ServiceProvider": { + "type": "CentralTransitive", + "requested": "[10.7.2, )", + "resolved": "10.7.2", + "contentHash": "Qa8sKt1i9Fy/zCw5GwAUsfT+lt4BvkIgYh8sRJ6fvqJWoedS//pfcyiKUUb0wL3C5Wrpi3U+vRud5DCbMHaFIw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[8.0.0, 11.0.0)", + "Microsoft.Extensions.Hosting.Abstractions": "[6.0.0, 11.0.0)", + "Microsoft.Extensions.Logging.Abstractions": "[6.0.0, 11.0.0)", + "Rebus": "8.9.0" + } + }, + "Scrutor": { + "type": "CentralTransitive", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "wHWaroody48jnlLoq/REwUltIFLxplXyHTP+sttrc8P7+jkiVqf38afDidJNv4qgD/6zz2NKOYg06xLZ1bG7wQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0" + } + }, + "Serilog": { + "type": "CentralTransitive", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" + }, + "Serilog.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", + "dependencies": { + "Serilog": "4.3.0", + "Serilog.Extensions.Hosting": "10.0.0", + "Serilog.Formatting.Compact": "3.0.0", + "Serilog.Settings.Configuration": "10.0.0", + "Serilog.Sinks.Console": "6.1.1", + "Serilog.Sinks.Debug": "3.0.0", + "Serilog.Sinks.File": "7.0.0" + } + }, + "Serilog.Enrichers.Environment": { + "type": "CentralTransitive", + "requested": "[3.0.1, )", + "resolved": "3.0.1", + "contentHash": "9BqCE4C9FF+/rJb/CsQwe7oVf44xqkOvMwX//CUxvUR25lFL4tSS6iuxE5eW07quby1BAyAEP+vM6TWsnT3iqw==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Process": { + "type": "CentralTransitive", + "requested": "[3.0.0, )", + "resolved": "3.0.0", + "contentHash": "/wPYz2PDCJGSHNI+Z0PAacZvrgZgrGduWqLXeC2wvW6pgGM/Bi45JrKy887MRcRPHIZVU0LAlkmJ7TkByC0boQ==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Enrichers.Thread": { + "type": "CentralTransitive", + "requested": "[4.0.0, )", + "resolved": "4.0.0", + "contentHash": "C7BK25a1rhUyr+Tp+1BYcVlBJq7M2VCHlIgnwoIUVJcicM9jYcvQK18+OeHiXw7uLPSjqWxJIp1EfaZ/RGmEwA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "CentralTransitive", + "requested": "[10.0.0, )", + "resolved": "10.0.0", + "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "10.0.0", + "Microsoft.Extensions.DependencyModel": "10.0.0", + "Serilog": "4.3.0" + } + }, + "Serilog.Sinks.Console": { + "type": "CentralTransitive", + "requested": "[6.1.1, )", + "resolved": "6.1.1", + "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", + "dependencies": { + "Serilog": "4.0.0" + } + }, + "Serilog.Sinks.Seq": { + "type": "CentralTransitive", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==", + "dependencies": { + "Serilog": "4.2.0", + "Serilog.Sinks.File": "6.0.0" + } + }, + "Swashbuckle.AspNetCore": { + "type": "CentralTransitive", + "requested": "[10.1.7, )", + "resolved": "10.1.7", + "contentHash": "vgef8DPT411JU5JjHiDbr0WOxsIVuAvegPGtqmm4Na4JRl/264dfBJcGkiPHsAr5P+Vda+qN1rZKRtBl1rF9aA==", + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "10.0.0", + "Swashbuckle.AspNetCore.Swagger": "10.1.7", + "Swashbuckle.AspNetCore.SwaggerGen": "10.1.7", + "Swashbuckle.AspNetCore.SwaggerUI": "10.1.7" + } + }, + "Swashbuckle.AspNetCore.Annotations": { + "type": "CentralTransitive", + "requested": "[10.1.7, )", + "resolved": "10.1.7", + "contentHash": "qHNQghKAzrjDEECIlyKEanmWxXDzrh6PyjGWVdql2qZfap0Ionuxam5OrUozibjbgQbVjRmgSXxWC8giIuCbGA==", + "dependencies": { + "Swashbuckle.AspNetCore.SwaggerGen": "10.1.7" + } + }, + "Swashbuckle.AspNetCore.SwaggerGen": { + "type": "CentralTransitive", + "requested": "[10.1.7, )", + "resolved": "10.1.7", + "contentHash": "PuubO9BjvNn6U3D9kLpuWKY1JtziWw7SsGBq0age1E50uQjQ8Fzl8s0EwzrLfANqYJNgDnJi9l7N1QxcGVB2Zw==", + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "10.1.7" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "CentralTransitive", + "requested": "[8.17.0, )", + "resolved": "8.17.0", + "contentHash": "nKikRYheDeSaXA3wGr2otwaiRFygBa25m+hc7MEomZVIEWZvKVqd8wgP9yn+8QpLRGgw//dUs4LErGx9gtVmAA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "8.17.0", + "Microsoft.IdentityModel.Tokens": "8.17.0" + } + }, + "System.IO.Hashing": { + "type": "CentralTransitive", + "requested": "[10.0.5, )", + "resolved": "10.0.5", + "contentHash": "8IBJWcCT9+e4Bmevm4T7+fQEiAh133KGiz4oiVTgJckd3Q76OFdR1falgn9lpz7+C4HJvogCDJeAa2QmvbeVtg==" + }, + "Testcontainers.Azurite": { + "type": "CentralTransitive", + "requested": "[4.11.0, )", + "resolved": "4.11.0", + "contentHash": "fDML8KiS9alTzmXD8gZziRZWNQDeElejBWr+Wg5vpg6IsK2PhM94coQDNmPyabTpuDQ8XopqxJIw2hyv7YdGqw==", + "dependencies": { + "Testcontainers": "4.11.0" + } + }, + "Testcontainers.RabbitMq": { + "type": "CentralTransitive", + "requested": "[4.11.0, )", + "resolved": "4.11.0", + "contentHash": "+4rDOXdjwwDx11vUpSAAK/gDOPtZ/LC+qj02RiulHtIMEAzC4KjyP5hEactvwSuGfcomFnmtJqYn1Pwyh+pPDw==", + "dependencies": { + "Testcontainers": "4.11.0" + } + } + } + } +} \ No newline at end of file diff --git a/src/Modules/SearchProviders/Infrastructure/Events/Handlers/ReviewApprovedIntegrationEventHandler.cs b/src/Modules/SearchProviders/Infrastructure/Events/Handlers/ReviewApprovedIntegrationEventHandler.cs new file mode 100644 index 000000000..71c004bec --- /dev/null +++ b/src/Modules/SearchProviders/Infrastructure/Events/Handlers/ReviewApprovedIntegrationEventHandler.cs @@ -0,0 +1,47 @@ +using MeAjudaAi.Modules.SearchProviders.Domain.Repositories; +using MeAjudaAi.Shared.Events; +using MeAjudaAi.Shared.Messaging.Messages.Ratings; +using Microsoft.Extensions.Logging; + +namespace MeAjudaAi.Modules.SearchProviders.Infrastructure.Events.Handlers; + +/// +/// Handler para processar o evento de aprovação de uma avaliação. +/// Atualiza a média de avaliação do provedor no módulo de busca. +/// +public sealed class ReviewApprovedIntegrationEventHandler( + ISearchableProviderRepository repository, + ILogger logger) : IEventHandler +{ + public async Task HandleAsync(ReviewApprovedIntegrationEvent integrationEvent, CancellationToken cancellationToken = default) + { + try + { + logger.LogInformation( + "Updating rating for provider {ProviderId} to {NewAverageRating} ({TotalReviews} reviews)", + integrationEvent.ProviderId, + integrationEvent.NewAverageRating, + integrationEvent.TotalReviews); + + var provider = await repository.GetByProviderIdAsync(integrationEvent.ProviderId, cancellationToken); + + if (provider == null) + { + logger.LogWarning("Provider {ProviderId} not found in search module when updating rating", integrationEvent.ProviderId); + return; + } + + provider.UpdateRating(integrationEvent.NewAverageRating, integrationEvent.TotalReviews); + + await repository.UpdateAsync(provider, cancellationToken); + await repository.SaveChangesAsync(cancellationToken); + + logger.LogInformation("Successfully updated rating for provider {ProviderId} in search module", integrationEvent.ProviderId); + } + catch (Exception ex) + { + logger.LogError(ex, "Error handling ReviewApprovedIntegrationEvent for provider {ProviderId}", integrationEvent.ProviderId); + throw; // Rethrow para permitir retry pelo broker + } + } +} diff --git a/src/Modules/SearchProviders/Infrastructure/Extensions.cs b/src/Modules/SearchProviders/Infrastructure/Extensions.cs index e744dc5e4..9dec827f1 100644 --- a/src/Modules/SearchProviders/Infrastructure/Extensions.cs +++ b/src/Modules/SearchProviders/Infrastructure/Extensions.cs @@ -5,13 +5,13 @@ using MeAjudaAi.Shared.Database; using MeAjudaAi.Shared.Events; using MeAjudaAi.Shared.Messaging.Messages.Providers; +using MeAjudaAi.Shared.Messaging.Messages.Ratings; +using MeAjudaAi.Shared.Messaging.Messages.ServiceCatalogs; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using MeAjudaAi.Shared.Messaging.Messages.ServiceCatalogs; - namespace MeAjudaAi.Modules.SearchProviders.Infrastructure; /// @@ -19,87 +19,54 @@ namespace MeAjudaAi.Modules.SearchProviders.Infrastructure; /// public static class Extensions { - /// - /// Registra serviços da camada de Infrastructure do SearchProviders. - /// - /// A coleção de serviços. - /// A configuração para ler strings de conexão e configurações. - /// O ambiente de hospedagem para determinar comportamento em Testing. - /// A coleção de serviços para encadeamento. public static IServiceCollection AddSearchProvidersInfrastructure( this IServiceCollection services, IConfiguration configuration, IHostEnvironment environment) { - // Registrar DbContext com suporte PostGIS - // IMPORTANTE: EF Core usa GetConnectionString("DefaultConnection") enquanto Dapper (via PostgresOptions) - // resolve de "Postgres:ConnectionString" ou "ConnectionStrings:meajudaai-db". - // Certifique-se de que estas chaves de configuração apontem para o mesmo database para evitar que EF e Dapper - // se comuniquem com databases/schemas diferentes entre ambientes (dev/test/prod, Aspire, etc.). - var connectionString = configuration.GetConnectionString("DefaultConnection") - ?? configuration.GetConnectionString("Search") - ?? configuration.GetConnectionString("meajudaai-db"); + services.AddPersistence(configuration, environment); + services.AddEventHandlers(); + + return services; + } - // Em ambiente de teste, permitir inicialização sem connection string - // (útil para testes unitários que não acessam o banco) - // Só fazemos bypass se estivermos explicitamente em Desenvolvimento e NÃO for um live environment - var isTesting = MeAjudaAi.Shared.Utilities.EnvironmentHelpers.IsSecurityBypassEnvironment(environment); + private static IServiceCollection AddPersistence( + this IServiceCollection services, + IConfiguration configuration, + IHostEnvironment environment) + { + var connectionString = configuration.GetConnectionString("DefaultConnection"); - if (string.IsNullOrEmpty(connectionString)) + if (string.IsNullOrWhiteSpace(connectionString) && !MeAjudaAi.Shared.Utilities.EnvironmentHelpers.IsSecurityBypassEnvironment(environment)) { - if (isTesting) - { -#pragma warning disable S2068 // "password" detected here, make sure this is not a hard-coded credential - connectionString = MeAjudaAi.Shared.Database.DatabaseConstants.DefaultTestConnectionString; -#pragma warning restore S2068 - } - else - { - var env1 = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); - var env2 = Environment.GetEnvironmentVariable("INTEGRATION_TESTS"); - var env3 = environment?.EnvironmentName; - throw new InvalidOperationException( - $"DEBUG: isTesting={isTesting}, ASPNETCORE={env1}, INTEGRATION_TESTS={env2}, EnvName={env3} " + - "Database connection string not found. Tried: 'DefaultConnection', 'Search', 'meajudaai-db'."); - } + throw new InvalidOperationException( + "Database connection string is not configured. " + + "Please set one of the following configuration keys: " + + "'ConnectionStrings:DefaultConnection', 'ConnectionStrings:Search', or 'ConnectionStrings:meajudaai-db'"); } - // Sempre registrar DbContext (mesmo que connection string seja vazia em testes unitários) - // Em E2E tests, a connection string será fornecida via configuração - services.AddDbContext((serviceProvider, options) => + // DbContext principal para escrita/comandos (EF Core) + services.AddDbContext(options => { - options.UseNpgsql(connectionString, npgsqlOptions => - { - npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "search_providers"); - npgsqlOptions.UseNetTopologySuite(); // Habilitar suporte PostGIS/geoespacial - }); - - options.UseSnakeCaseNamingConvention(); - - // Suprimir o warning PendingModelChangesWarning apenas em ambiente de desenvolvimento - if (environment.IsDevelopment()) + if (!string.IsNullOrWhiteSpace(connectionString)) { - options.ConfigureWarnings(warnings => - warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)); + options.UseNpgsql(connectionString, npgsqlOptions => + { + npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", "search_providers"); + npgsqlOptions.EnableRetryOnFailure(3); + }); } - // Habilitar erros detalhados em desenvolvimento - if (configuration.GetValue("DetailedErrors")) + if (environment.IsDevelopment()) { - options.EnableDetailedErrors(); options.EnableSensitiveDataLogging(); + options.EnableDetailedErrors(); } }); - // Registrar Dapper para queries espaciais otimizadas - services.AddDapper(); - - // Registrar repositórios + // Repositórios services.AddScoped(); - // Registrar Event Handlers (sempre necessário, independente de connection string) - services.AddEventHandlers(); - return services; } @@ -112,6 +79,7 @@ private static IServiceCollection AddEventHandlers(this IServiceCollection servi services.AddScoped, ProviderActivatedIntegrationEventHandler>(); services.AddScoped, ProviderServicesUpdatedIntegrationEventHandler>(); services.AddScoped, ServiceDeactivatedIntegrationEventHandler>(); + services.AddScoped, ReviewApprovedIntegrationEventHandler>(); return services; } diff --git a/src/Modules/SearchProviders/Tests/Unit/API/ModuleExtensionsTests.cs b/src/Modules/SearchProviders/Tests/Unit/API/ModuleExtensionsTests.cs index d125532b4..2980a5280 100644 --- a/src/Modules/SearchProviders/Tests/Unit/API/ModuleExtensionsTests.cs +++ b/src/Modules/SearchProviders/Tests/Unit/API/ModuleExtensionsTests.cs @@ -76,7 +76,7 @@ public void AddSearchProvidersModule_WithEmptyConfiguration_ShouldThrowException var exception = Assert.Throws(() => services.AddSearchProvidersModule(configuration, mockEnv.Object)); - Assert.Contains("Database connection string not found", exception.Message); + Assert.Contains("Database connection string is not configured", exception.Message); } [Theory] diff --git a/src/Modules/SearchProviders/Tests/packages.lock.json b/src/Modules/SearchProviders/Tests/packages.lock.json index d1f46647e..38e4be4a8 100644 --- a/src/Modules/SearchProviders/Tests/packages.lock.json +++ b/src/Modules/SearchProviders/Tests/packages.lock.json @@ -1014,6 +1014,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1171,6 +1172,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs b/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs index fc337e222..e5be9c107 100644 --- a/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs +++ b/src/Modules/ServiceCatalogs/API/Endpoints/Service/GetServicesByCategoryEndpoint.cs @@ -35,11 +35,11 @@ Retorna todos os serviços de uma categoria específica. private static async Task GetByCategoryAsync( Guid categoryId, - [AsParameters] GetServicesByCategoryQuery query, + [FromQuery] bool? activeOnly, IQueryDispatcher queryDispatcher, CancellationToken cancellationToken) { - var queryWithCategory = query with { CategoryId = categoryId }; + var queryWithCategory = new GetServicesByCategoryQuery(categoryId, activeOnly ?? false); var result = await queryDispatcher.QueryAsync>>(queryWithCategory, cancellationToken); return Handle(result); } diff --git a/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs index 34a044110..0f06684f1 100644 --- a/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs +++ b/src/Modules/ServiceCatalogs/Application/Queries/Service/GetServicesByCategoryQuery.cs @@ -5,7 +5,9 @@ namespace MeAjudaAi.Modules.ServiceCatalogs.Application.Queries.Service; /// -/// Query to retrieve all services within a specific category. +/// Consulta para recuperar todos os serviços de uma categoria específica. /// +/// ID da categoria para filtrar os serviços. +/// Se verdadeiro, retorna apenas os serviços ativos da categoria. public sealed record GetServicesByCategoryQuery(Guid CategoryId, bool ActiveOnly = false) : Query>>; diff --git a/src/Modules/ServiceCatalogs/Tests/packages.lock.json b/src/Modules/ServiceCatalogs/Tests/packages.lock.json index d1f46647e..38e4be4a8 100644 --- a/src/Modules/ServiceCatalogs/Tests/packages.lock.json +++ b/src/Modules/ServiceCatalogs/Tests/packages.lock.json @@ -1014,6 +1014,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1171,6 +1172,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/src/Modules/Users/API/API.Client/Public/GetAuthProviders.bru b/src/Modules/Users/API/API.Client/Public/GetAuthProviders.bru new file mode 100644 index 000000000..5d71d2560 --- /dev/null +++ b/src/Modules/Users/API/API.Client/Public/GetAuthProviders.bru @@ -0,0 +1,17 @@ +meta { + name: Get Auth Providers + type: http + seq: 2 +} + +get { + url: {{baseUrl}}/api/v1/users/auth-providers + body: none + auth: none +} + +docs { + # Get Auth Providers + + Retorna os provedores de autenticação externa configurados (Instagram, Google, etc). +} diff --git a/src/Modules/Users/API/API.Client/Public/RegisterCustomer.bru b/src/Modules/Users/API/API.Client/Public/RegisterCustomer.bru new file mode 100644 index 000000000..b8f747fe4 --- /dev/null +++ b/src/Modules/Users/API/API.Client/Public/RegisterCustomer.bru @@ -0,0 +1,30 @@ +meta { + name: Register Customer + type: http + seq: 1 +} + +post { + url: {{baseUrl}}/api/v1/users/register + body: json + auth: none +} + +headers { + Content-Type: application/json +} + +body:json { + { + "email": "customer@example.com", + "password": "Password123!", + "firstName": "John", + "lastName": "Doe" + } +} + +docs { + # Register Customer + + Registra um novo cliente no sistema. +} diff --git a/src/Modules/Users/Tests/packages.lock.json b/src/Modules/Users/Tests/packages.lock.json index d1f46647e..38e4be4a8 100644 --- a/src/Modules/Users/Tests/packages.lock.json +++ b/src/Modules/Users/Tests/packages.lock.json @@ -1014,6 +1014,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1171,6 +1172,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/src/Shared/Exceptions/ExceptionsExtensions.cs b/src/Shared/Exceptions/ExceptionsExtensions.cs index a4570a6a5..e9c63851e 100644 --- a/src/Shared/Exceptions/ExceptionsExtensions.cs +++ b/src/Shared/Exceptions/ExceptionsExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace MeAjudaAi.Shared.Exceptions; diff --git a/src/Shared/Exceptions/GlobalExceptionHandler.cs b/src/Shared/Exceptions/GlobalExceptionHandler.cs index 0eac994a2..9ee9a8e1f 100644 --- a/src/Shared/Exceptions/GlobalExceptionHandler.cs +++ b/src/Shared/Exceptions/GlobalExceptionHandler.cs @@ -129,7 +129,7 @@ public async ValueTask TryHandleAsync( _ => ( StatusCodes.Status500InternalServerError, "Erro Interno do Servidor", - "Ocorreu um erro inesperado ao processar sua requisição", + "Ocorreu um erro inesperado no servidor.", null, new Dictionary { diff --git a/src/Shared/Extensions/ServiceCollectionExtensions.cs b/src/Shared/Extensions/ServiceCollectionExtensions.cs index 07cf875a1..be470f4d7 100644 --- a/src/Shared/Extensions/ServiceCollectionExtensions.cs +++ b/src/Shared/Extensions/ServiceCollectionExtensions.cs @@ -37,6 +37,7 @@ public static IServiceCollection AddSharedServices( // Serilog configurado no Program.cs do ApiService services.AddPostgres(configuration); + services.AddDapper(); services.AddCaching(configuration); // Só adiciona messaging se não estiver em ambiente de teste diff --git a/src/Shared/Messaging/Messages/Ratings/ReviewApprovedIntegrationEvent.cs b/src/Shared/Messaging/Messages/Ratings/ReviewApprovedIntegrationEvent.cs new file mode 100644 index 000000000..f4a4b5ef6 --- /dev/null +++ b/src/Shared/Messaging/Messages/Ratings/ReviewApprovedIntegrationEvent.cs @@ -0,0 +1,17 @@ +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.Shared.Messaging.Messages.Ratings; + +/// +/// Evento de integração disparado quando uma nova avaliação é aprovada e incorporada à média do prestador. +/// +public record ReviewApprovedIntegrationEvent( + string Source, + Guid ProviderId, + Guid ReviewId, + decimal NewAverageRating, + int TotalReviews, + int ReviewRating, + string? ReviewComment, + DateTime CreatedAt +) : IntegrationEvent(Source); diff --git a/src/Shared/Utilities/ClaimHelpers.cs b/src/Shared/Utilities/ClaimHelpers.cs new file mode 100644 index 000000000..0ee5f5e48 --- /dev/null +++ b/src/Shared/Utilities/ClaimHelpers.cs @@ -0,0 +1,26 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Http; + +namespace MeAjudaAi.Shared.Utilities; + +public static class ClaimHelpers +{ + public static string? GetUserId(ClaimsPrincipal? user) + { + return user?.FindFirst("sub")?.Value + ?? user?.FindFirst("id")?.Value + ?? user?.FindFirst(ClaimTypes.NameIdentifier)?.Value; + } + + public static Guid? GetUserIdGuid(ClaimsPrincipal? user) + { + var id = GetUserId(user); + return Guid.TryParse(id, out var guid) ? guid : null; + } + + public static Guid? GetUserIdGuid(HttpContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + return GetUserIdGuid(context.User); + } +} diff --git a/src/Web/MeAjudaAi.Web.Admin/__tests__/hooks/admin/use-services.test.ts b/src/Web/MeAjudaAi.Web.Admin/__tests__/hooks/admin/use-services.test.ts index 36e1909aa..56500351a 100644 --- a/src/Web/MeAjudaAi.Web.Admin/__tests__/hooks/admin/use-services.test.ts +++ b/src/Web/MeAjudaAi.Web.Admin/__tests__/hooks/admin/use-services.test.ts @@ -34,7 +34,10 @@ describe('useServices Hook (Admin)', () => { await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(result.current.data).toHaveLength(1); - expect(api.apiCategoryGet).toHaveBeenCalledWith({ path: { categoryId: 'cat-123' } }); + expect(api.apiCategoryGet).toHaveBeenCalledWith({ + path: { categoryId: 'cat-123' }, + query: { activeOnly: false } + }); }); it('deve buscar serviço por ID', async () => { diff --git a/src/Web/MeAjudaAi.Web.Admin/hooks/admin/use-services.ts b/src/Web/MeAjudaAi.Web.Admin/hooks/admin/use-services.ts index c841cda63..49b7f0ce7 100644 --- a/src/Web/MeAjudaAi.Web.Admin/hooks/admin/use-services.ts +++ b/src/Web/MeAjudaAi.Web.Admin/hooks/admin/use-services.ts @@ -26,10 +26,15 @@ export const serviceKeys = { detail: (id: string) => [...serviceKeys.details(), id] as const, }; -export function useServices(categoryId?: string) { +export function useServices(categoryId?: string, activeOnly: boolean = false) { return useQuery({ - queryKey: serviceKeys.list({ categoryId } as any), - queryFn: () => categoryId ? apiCategoryGet({ path: { categoryId } }) : apiServicesGet(), + queryKey: serviceKeys.list({ categoryId, activeOnly } as any), + queryFn: () => categoryId + ? apiCategoryGet({ + path: { categoryId }, + query: { activeOnly } + } as any) + : apiServicesGet(), select: (data: any) => data.data ?? data, }); } diff --git a/tests/MeAjudaAi.ApiService.Tests/packages.lock.json b/tests/MeAjudaAi.ApiService.Tests/packages.lock.json index ebe0178f2..0581e6b7d 100644 --- a/tests/MeAjudaAi.ApiService.Tests/packages.lock.json +++ b/tests/MeAjudaAi.ApiService.Tests/packages.lock.json @@ -910,6 +910,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1067,6 +1068,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/tests/MeAjudaAi.Architecture.Tests/MeAjudaAi.Architecture.Tests.csproj b/tests/MeAjudaAi.Architecture.Tests/MeAjudaAi.Architecture.Tests.csproj index 04bfeb453..738cf4d45 100644 --- a/tests/MeAjudaAi.Architecture.Tests/MeAjudaAi.Architecture.Tests.csproj +++ b/tests/MeAjudaAi.Architecture.Tests/MeAjudaAi.Architecture.Tests.csproj @@ -31,6 +31,12 @@ + + + + + +
diff --git a/tests/MeAjudaAi.Architecture.Tests/packages.lock.json b/tests/MeAjudaAi.Architecture.Tests/packages.lock.json index a4246ed87..3b9bfb217 100644 --- a/tests/MeAjudaAi.Architecture.Tests/packages.lock.json +++ b/tests/MeAjudaAi.Architecture.Tests/packages.lock.json @@ -805,6 +805,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -962,6 +963,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/tests/MeAjudaAi.E2E.Tests/Base/BaseTestContainerTest.cs b/tests/MeAjudaAi.E2E.Tests/Base/BaseTestContainerTest.cs index 4d2826b37..1015e5386 100644 --- a/tests/MeAjudaAi.E2E.Tests/Base/BaseTestContainerTest.cs +++ b/tests/MeAjudaAi.E2E.Tests/Base/BaseTestContainerTest.cs @@ -3,8 +3,10 @@ using MeAjudaAi.ApiService; using MeAjudaAi.Modules.Documents.Application.Interfaces; using MeAjudaAi.Modules.Documents.Infrastructure.Persistence; +using MeAjudaAi.Modules.Communications.Infrastructure.Persistence; using MeAjudaAi.Modules.Documents.Tests.Mocks; using MeAjudaAi.Modules.Locations.Infrastructure.Persistence; +using MeAjudaAi.Modules.Ratings.Infrastructure.Persistence; using MeAjudaAi.Modules.Providers.Infrastructure.Persistence; using MeAjudaAi.Modules.SearchProviders.Infrastructure.Persistence; using MeAjudaAi.Modules.ServiceCatalogs.Infrastructure.Persistence; @@ -14,6 +16,7 @@ using MeAjudaAi.Shared.Database; using MeAjudaAi.Shared.Serialization; using MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers; +using MeAjudaAi.E2E.Tests.Base.Helpers; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; @@ -43,6 +46,12 @@ public abstract class BaseTestContainerTest : IAsyncLifetime private static RedisContainer? _redisContainer; private static AzuriteContainer? _azuriteContainer; + /// + /// Sobrescreva em classes derivadas para habilitar o message bus em memória síncrono e eventos de domínio. + /// Usado para testes que dependem de eventos de integração entre módulos (ex: Ratings -> SearchProviders). + /// + protected virtual bool EnableEventsAndMessageBus => false; + // Locking for thread-safe initialization private static readonly SemaphoreSlim _initializationLock = new(1, 1); private static bool _initialized = false; @@ -142,6 +151,7 @@ private void InitializeFactory() ["ConnectionStrings:meajudaai-db"] = _postgresContainer!.GetConnectionString(), ["ConnectionStrings:UsersDb"] = _postgresContainer!.GetConnectionString(), ["ConnectionStrings:ProvidersDb"] = _postgresContainer!.GetConnectionString(), + ["ConnectionStrings:RatingsDb"] = _postgresContainer!.GetConnectionString(), ["ConnectionStrings:DocumentsDb"] = _postgresContainer!.GetConnectionString(), ["ConnectionStrings:Redis"] = _redisContainer!.GetConnectionString(), ["Azure:Storage:ConnectionString"] = _azuriteContainer!.GetConnectionString(), @@ -155,7 +165,7 @@ private void InitializeFactory() ["ExternalServices:Keycloak:Enabled"] = "false", ["ExternalServices:PaymentGateway:Enabled"] = "false", ["ExternalServices:Geolocation:Enabled"] = "false", - ["Cache:Enabled"] = "false", // Desabilitar Redis por enquanto + ["Cache:Enabled"] = "false", ["Cache:ConnectionString"] = _redisContainer!.GetConnectionString(), // Desabilitar completamente Rate Limiting nos testes E2E ["AdvancedRateLimit:General:Enabled"] = "false", @@ -191,8 +201,11 @@ private void InitializeFactory() // Reconfigurar todos os DbContexts com TestContainer connection string ReconfigureDbContext(services); ReconfigureDbContext(services); + ReconfigureDbContext(services); ReconfigureDbContext(services); ReconfigureDbContext(services); + ReconfigureDbContext(services); + ReconfigureDbContext(services); ReconfigureSearchProvidersDbContext(services); // Configurar PostgresOptions e Dapper para SearchProviders @@ -239,24 +252,16 @@ private void InitializeFactory() services.AddScoped(); - // Substituir IMessageBus por MockMessageBus para testes (RabbitMQ desabilitado) - // Remover TODAS as implementações existentes de IMessageBus e adicionar o mock - var messageBusDescriptors = services.Where(d => d.ServiceType == typeof(MeAjudaAi.Shared.Messaging.IMessageBus)).ToList(); - foreach (var descriptor in messageBusDescriptors) + if (EnableEventsAndMessageBus) { - services.Remove(descriptor); + ReplaceService(services, ServiceLifetime.Singleton); + ReplaceService(services, ServiceLifetime.Scoped); } - - // Adicionar como Singleton com factory para injetar IServiceProvider - services.AddSingleton(); - - // Substituir IDomainEventProcessor por um mock que não processa eventos - var domainEventProcessorDescriptors = services.Where(d => d.ServiceType == typeof(MeAjudaAi.Shared.Events.IDomainEventProcessor)).ToList(); - foreach (var descriptor in domainEventProcessorDescriptors) + else { - services.Remove(descriptor); + ReplaceService(services, ServiceLifetime.Singleton); + ReplaceService(services, ServiceLifetime.Scoped); } - services.AddScoped(); // Remove todas as configurações de autenticação existentes var authDescriptors = services @@ -344,53 +349,99 @@ private async Task ApplyMigrationsAsync() if (_migrationsApplied) return; using var scope = _factory.Services.CreateScope(); + var services = scope.ServiceProvider; - // Garantir que o banco está limpo primeiro se necessário - var usersContext = scope.ServiceProvider.GetRequiredService(); - await usersContext.Database.EnsureDeletedAsync(); - - // 1. Users (Core) - await usersContext.Database.MigrateAsync(); - - // 2. ServiceCatalogs (Critical Prerequisite for Providers) - var catalogsContext = scope.ServiceProvider.GetRequiredService(); - await catalogsContext.Database.MigrateAsync(); - - // 3. Providers (Depends on ServiceCatalogs) - var providersContext = scope.ServiceProvider.GetRequiredService(); - await providersContext.Database.MigrateAsync(); - - // 4. Documents - var documentsContext = scope.ServiceProvider.GetRequiredService(); - await documentsContext.Database.MigrateAsync(); - - // 5. Locations - var locationsContext = scope.ServiceProvider.GetRequiredService(); - await locationsContext.Database.MigrateAsync(); + // Apply migrations for all DbContexts using helper + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); // 6. SearchProviders (Depends on Providers + PostGIS) - try + var searchContext = scope.ServiceProvider.GetService(); + if (searchContext != null) { - var searchContext = scope.ServiceProvider.GetService(); - if (searchContext != null) - { - await searchContext.Database.MigrateAsync(); - } - } - catch (Exception ex) - { - // Log but rethrow to avoid masking issues - throw new Exception($"Failed to apply SearchProviders migrations: {ex.Message}", ex); + await MigrationTestHelper.ApplyMigrationForContext(searchContext); } _migrationsApplied = true; } + catch (Exception ex) + { + throw new Exception($"Failed to apply migrations: {ex.Message}", ex); + } finally { _migrationLock.Release(); } } + /// + /// Limpa todas as tabelas de todos os contextos registrados e o cache do Redis para garantir isolamento entre testes. + /// + /// + /// Este método não é chamado automaticamente pelo ou . + /// Deve ser invocado explicitamente por testes derivados quando o isolamento do teste exigir a limpeza do estado + /// do banco de dados e do Redis. + /// + /// Uso típico: Chamada no início ou no final de um teste ou fixture, por exemplo: await CleanupDatabaseAsync(); + /// + /// + /// Efeitos colaterais: Limpa todos os DbContexts registrados via + /// e executa um FLUSHALL no Redis se _redisContainer estiver disponível. + /// + /// + protected async Task CleanupDatabaseAsync() + { + using var scope = _factory.Services.CreateScope(); + var services = scope.ServiceProvider; + + await CleanupContext(services); + await CleanupContext(services); + await CleanupContext(services); + await CleanupContext(services); + await CleanupContext(services); + await CleanupContext(services); + await CleanupContext(services); + await CleanupContext(services); + + if (_redisContainer != null) + { + await _redisContainer.ExecAsync(new[] { "redis-cli", "FLUSHALL" }); + } + } + + private static async Task CleanupContext(IServiceProvider services) where TContext : DbContext + { + var context = services.GetRequiredService(); + var tableNames = new List(); + + foreach (var entityType in context.Model.GetEntityTypes()) + { + var tableName = entityType.GetTableName(); + var schema = entityType.GetSchema(); + + if (!string.IsNullOrEmpty(tableName)) + { + var qualifiedTableName = string.IsNullOrEmpty(schema) || schema == "public" + ? $"\"{tableName}\"" + : $"\"{schema}\".\"{tableName}\""; + + tableNames.Add(qualifiedTableName); + } + } + + if (tableNames.Count > 0) + { + var uniqueTables = tableNames.Distinct().ToList(); + var batchSql = $"TRUNCATE TABLE {string.Join(", ", uniqueTables)} CASCADE"; + await context.Database.ExecuteSqlRawAsync(batchSql); + } + } + // Helper methods usando serialização compartilhada #pragma warning disable CA2000 // Dispose StringContent - handled by HttpClient /// @@ -523,17 +574,24 @@ protected async Task PutJsonAsync(Uri requestUri, T cont /// private void ReconfigureDbContext(IServiceCollection services) where TContext : DbContext { + var contextName = typeof(TContext).Name; var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) services.Remove(descriptor); services.AddDbContext(options => { - options.UseNpgsql(_postgresContainer.GetConnectionString()) - .UseSnakeCaseNamingConvention() - .EnableSensitiveDataLogging(true) // Useful for test debugging - .ConfigureWarnings(warnings => - warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)); + options.UseNpgsql(_postgresContainer!.GetConnectionString(), npgsqlOptions => + { + npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", DbContextSchemaHelper.GetSchemaName(contextName)); + npgsqlOptions.MigrationsAssembly(typeof(TContext).Assembly.FullName); + npgsqlOptions.UseNetTopologySuite(); + npgsqlOptions.CommandTimeout(120); + }) + .UseSnakeCaseNamingConvention() + .EnableSensitiveDataLogging(true) + .ConfigureWarnings(warnings => + warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)); }); } @@ -605,7 +663,8 @@ protected async Task CreateTestUserAsync(string? username = null, string? var response = await ApiClient.PostAsJsonAsync("/api/v1/users", createRequest, JsonOptions); if (response.StatusCode != HttpStatusCode.Created) { - throw new InvalidOperationException($"Failed to create test user. Status: {response.StatusCode}"); + var body = await response.Content.ReadAsStringAsync(); + throw new InvalidOperationException($"Failed to create test user. Status: {response.StatusCode}, Body: {body}"); } var locationHeader = response.Headers.Location?.ToString(); @@ -638,39 +697,18 @@ protected override async Task SendAsync( } /// - /// Mock implementation of IMessageBus for E2E tests. - /// Does not process events to avoid deadlocks - tests should use APIs directly. + /// Helper para substituir um serviço existente na IServiceCollection. /// - private class MockMessageBus : MeAjudaAi.Shared.Messaging.IMessageBus + protected static void ReplaceService(IServiceCollection services, ServiceLifetime lifetime) + where TImplementation : class, TService { - public Task SendAsync(TMessage message, string? queueName = null, CancellationToken cancellationToken = default) + var descriptors = services.Where(d => d.ServiceType == typeof(TService)).ToList(); + foreach (var descriptor in descriptors) { - return Task.CompletedTask; - } - - public Task PublishAsync(TMessage @event, string? topicName = null, CancellationToken cancellationToken = default) - { - // No-op: E2E tests should trigger actions via HTTP APIs, not via events - return Task.CompletedTask; - } - - public Task SubscribeAsync(Func handler, string? subscriptionName = null, CancellationToken cancellationToken = default) - { - // No-op: subscriptions are not actually created in E2E tests - return Task.CompletedTask; + services.Remove(descriptor); } - } - /// - /// Mock implementation of IDomainEventProcessor for E2E tests - /// Does not process domain events to avoid integration event publication - /// - private class MockDomainEventProcessor : MeAjudaAi.Shared.Events.IDomainEventProcessor - { - public Task ProcessDomainEventsAsync(IEnumerable domainEvents, CancellationToken cancellationToken = default) - { - // No-op: domain events are not processed in E2E tests to avoid integration event publication - return Task.CompletedTask; - } + var newDescriptor = new ServiceDescriptor(typeof(TService), typeof(TImplementation), lifetime); + services.Add(newDescriptor); } } diff --git a/tests/MeAjudaAi.E2E.Tests/Base/Helpers/DbContextSchemaHelper.cs b/tests/MeAjudaAi.E2E.Tests/Base/Helpers/DbContextSchemaHelper.cs new file mode 100644 index 000000000..ae99b56ef --- /dev/null +++ b/tests/MeAjudaAi.E2E.Tests/Base/Helpers/DbContextSchemaHelper.cs @@ -0,0 +1,20 @@ +namespace MeAjudaAi.E2E.Tests.Base.Helpers; + +public static class DbContextSchemaHelper +{ + public static string GetSchemaName(string contextName) + { + return contextName switch + { + "UsersDbContext" => "users", + "ProvidersDbContext" => "providers", + "DocumentsDbContext" => "documents", + "ServiceCatalogsDbContext" => "service_catalogs", + "LocationsDbContext" => "locations", + "CommunicationsDbContext" => "communications", + "SearchProvidersDbContext" => "search_providers", + "RatingsDbContext" => "ratings", + _ => "public" + }; + } +} diff --git a/tests/MeAjudaAi.E2E.Tests/Base/Helpers/MigrationTestHelper.cs b/tests/MeAjudaAi.E2E.Tests/Base/Helpers/MigrationTestHelper.cs new file mode 100644 index 000000000..4794b825a --- /dev/null +++ b/tests/MeAjudaAi.E2E.Tests/Base/Helpers/MigrationTestHelper.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; + +namespace MeAjudaAi.E2E.Tests.Base.Helpers; + +public static class MigrationTestHelper +{ + public static async Task ApplyMigrationForContext(DbContext context) + { + var contextName = context.GetType().Name; + + // Garantir que o esquema exista antes da migração + var schema = DbContextSchemaHelper.GetSchemaName(contextName); + if (schema != "public") + { + await context.Database.ExecuteSqlRawAsync($"CREATE SCHEMA IF NOT EXISTS \"{schema}\";"); + } + + await context.Database.MigrateAsync(); + } +} diff --git a/tests/MeAjudaAi.E2E.Tests/Base/Mocks.cs b/tests/MeAjudaAi.E2E.Tests/Base/Mocks.cs new file mode 100644 index 000000000..8c4eaff62 --- /dev/null +++ b/tests/MeAjudaAi.E2E.Tests/Base/Mocks.cs @@ -0,0 +1,41 @@ +using MeAjudaAi.Shared.Messaging; +using MeAjudaAi.Shared.Events; + +namespace MeAjudaAi.E2E.Tests.Base; + +/// +/// Implementação mock de IMessageBus para testes E2E. +/// Não processa eventos para evitar deadlocks - os testes devem usar as APIs diretamente. +/// +internal class MockMessageBus : IMessageBus +{ + public Task SendAsync(TMessage message, string? queueName = null, CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task PublishAsync(TMessage @event, string? topicName = null, CancellationToken cancellationToken = default) + { + // No-op: testes E2E devem disparar ações via APIs HTTP, não via eventos + return Task.CompletedTask; + } + + public Task SubscribeAsync(Func handler, string? subscriptionName = null, CancellationToken cancellationToken = default) + { + // No-op: assinaturas não são realmente criadas em testes E2E + return Task.CompletedTask; + } +} + +/// +/// Implementação mock de IDomainEventProcessor usada em testes E2E; +/// não processa eventos de domínio para evitar publicação de eventos de integração. +/// +internal class MockDomainEventProcessor : IDomainEventProcessor +{ + public Task ProcessDomainEventsAsync(IEnumerable domainEvents, CancellationToken cancellationToken = default) + { + // No-op: eventos de domínio não são processados em testes E2E para evitar publicação de eventos de integração + return Task.CompletedTask; + } +} diff --git a/tests/MeAjudaAi.E2E.Tests/Base/TestContainerFixture.cs b/tests/MeAjudaAi.E2E.Tests/Base/TestContainerFixture.cs index c25439d92..29d4d43d9 100644 --- a/tests/MeAjudaAi.E2E.Tests/Base/TestContainerFixture.cs +++ b/tests/MeAjudaAi.E2E.Tests/Base/TestContainerFixture.cs @@ -7,6 +7,7 @@ using MeAjudaAi.Modules.Users.Tests.Infrastructure.Mocks; using MeAjudaAi.Shared.Database; using MeAjudaAi.Shared.Serialization; +using MeAjudaAi.E2E.Tests.Base.Helpers; using Microsoft.AspNetCore.Hosting; using System.Net.Http.Json; using Microsoft.AspNetCore.Mvc.Testing; @@ -43,6 +44,12 @@ public class TestContainerFixture : IAsyncLifetime public string AzuriteConnectionString { get; private set; } = null!; public Faker Faker { get; } = new(); + /// + /// Define se o sistema de mensagens síncrono e processamento de eventos devem estar ativos. + /// Por padrão é falso para isolar testes unitários/E2E simples. + /// + public virtual bool EnableEventsAndMessageBus => false; + public static System.Text.Json.JsonSerializerOptions JsonOptions => SerializationDefaults.Api; public async ValueTask InitializeAsync() @@ -123,7 +130,6 @@ private async Task InitializeContainersAsync() // Iniciar containers em paralelo var startTasks = new List(); - // Simple unconditional start - let Testcontainers handle idempotency or just fail if really broken startTasks.Add(_postgresContainer.StartAsync()); startTasks.Add(_redisContainer.StartAsync()); startTasks.Add(_azuriteContainer.StartAsync()); @@ -152,7 +158,6 @@ private async Task InitializeFactoryAsync() { config.AddInMemoryCollection(new Dictionary { - // All modules share the same test database instance ["ConnectionStrings:DefaultConnection"] = PostgresConnectionString, ["ConnectionStrings:meajudaai-db"] = PostgresConnectionString, ["ConnectionStrings:Users"] = PostgresConnectionString, @@ -172,7 +177,7 @@ private async Task InitializeFactoryAsync() ["Keycloak:ClientSecret"] = "test-secret", ["Keycloak:AdminUsername"] = "test-admin", ["Keycloak:AdminPassword"] = "test-password", - ["Cache:Enabled"] = "true", // Enable cache for realistic E2E testing + ["Cache:Enabled"] = "false", // Sincronizado com BaseTestContainerTest ["Cache:ConnectionString"] = RedisConnectionString, ["AdvancedRateLimit:General:Enabled"] = "false", ["AdvancedRateLimit:Anonymous:RequestsPerMinute"] = "10000", @@ -194,22 +199,17 @@ private async Task InitializeFactoryAsync() builder.ConfigureServices(services => { - // Configurar logging mínimo para testes services.AddLogging(logging => { logging.ClearProviders(); logging.SetMinimumLevel(LogLevel.Error); }); - // Mock de serviços externos ConfigureMockServices(services); - - // Reconfigurar DbContexts ReconfigureDbContexts(services); }); }); - // Create API client com handler que propaga contexto de teste var contextPropagationHandler = new TestContextAwareHandler { InnerHandler = _factory.Server.CreateHandler() @@ -221,46 +221,50 @@ private async Task InitializeFactoryAsync() }; Services = _factory.Services; - - // Aplicar migrations e seed inicial: MOVED TO InitializeAsync to ensure run-once semantics - // await ApplyMigrationsAsync(); } private void ConfigureMockServices(IServiceCollection services) { - // CRITICAL: Substituir autenticação real por ConfigurableTestAuthenticationHandler - // NÃO remover services de autenticação - apenas substituir o scheme padrão - - // Adicionar/substituir test authentication com ConfigurableTestAuthenticationHandler services.AddAuthentication(MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers.ConfigurableTestAuthenticationHandler.SchemeName) .AddScheme( MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers.ConfigurableTestAuthenticationHandler.SchemeName, _ => { }); - // Garantir que authorization está configurado if (!services.Any(d => d.ServiceType == typeof(Microsoft.AspNetCore.Authorization.IAuthorizationService))) { services.AddAuthorization(); } - // Mock Keycloak - var keycloakDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IKeycloakService)); - if (keycloakDescriptor != null) - services.Remove(keycloakDescriptor); + var keycloakDescriptors = services.Where(d => d.ServiceType == typeof(IKeycloakService)).ToList(); + foreach (var d in keycloakDescriptors) services.Remove(d); services.AddSingleton(); - // Mock Blob Storage - var blobDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IBlobStorageService)); - if (blobDescriptor != null) - services.Remove(blobDescriptor); + var blobDescriptors = services.Where(d => d.ServiceType == typeof(IBlobStorageService)).ToList(); + foreach (var d in blobDescriptors) services.Remove(d); services.AddSingleton(); - // Mock Document Intelligence - var ocrDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IDocumentIntelligenceService)); - if (ocrDescriptor != null) - services.Remove(ocrDescriptor); + var ocrDescriptors = services.Where(d => d.ServiceType == typeof(IDocumentIntelligenceService)).ToList(); + foreach (var d in ocrDescriptors) services.Remove(d); services.AddSingleton(); + + // Message Bus Condicional para E2E + var busDescriptors = services.Where(d => d.ServiceType == typeof(MeAjudaAi.Shared.Messaging.IMessageBus)).ToList(); + foreach (var d in busDescriptors) services.Remove(d); + + var domainProcessorDescriptors = services.Where(d => d.ServiceType == typeof(MeAjudaAi.Shared.Events.IDomainEventProcessor)).ToList(); + foreach (var d in domainProcessorDescriptors) services.Remove(d); + + if (EnableEventsAndMessageBus) + { + services.AddSingleton(); + services.AddScoped(); + } + else + { + services.AddSingleton(); + services.AddScoped(); + } } private void ReconfigureDbContexts(IServiceCollection services) @@ -270,25 +274,21 @@ private void ReconfigureDbContexts(IServiceCollection services) ReconfigureDbContext(services); ReconfigureDbContext(services); ReconfigureDbContext(services); + ReconfigureDbContext(services); ReconfigureDbContext(services); + ReconfigureDbContext(services); - // PostgresOptions para SearchProviders (Dapper) var postgresOptionsDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(PostgresOptions)); if (postgresOptionsDescriptor != null) services.Remove(postgresOptionsDescriptor); - services.AddSingleton(new PostgresOptions - { - ConnectionString = PostgresConnectionString - }); + services.AddSingleton(new PostgresOptions { ConnectionString = PostgresConnectionString }); - // DatabaseMetrics if (!services.Any(d => d.ServiceType == typeof(DatabaseMetrics))) { services.AddSingleton(); } - // DapperConnection var dapperDescriptor = services.SingleOrDefault(d => d.ServiceType == typeof(IDapperConnection)); if (dapperDescriptor != null) services.Remove(dapperDescriptor); @@ -298,6 +298,9 @@ private void ReconfigureDbContexts(IServiceCollection services) private void ReconfigureDbContext(IServiceCollection services) where TContext : DbContext { + var contextName = typeof(TContext).Name; + // Console.WriteLine($"[DEBUG] Reconfiguring DbContext: {contextName}"); + var descriptor = services.SingleOrDefault(d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) services.Remove(descriptor); @@ -306,15 +309,13 @@ private void ReconfigureDbContext(IServiceCollection services) where T { options.UseNpgsql(PostgresConnectionString, npgsqlOptions => { + npgsqlOptions.MigrationsHistoryTable("__EFMigrationsHistory", DbContextSchemaHelper.GetSchemaName(contextName)); npgsqlOptions.MigrationsAssembly(typeof(TContext).Assembly.FullName); npgsqlOptions.UseNetTopologySuite(); - npgsqlOptions.CommandTimeout(120); // 2 minutos timeout para queries (WSL2 overhead) + npgsqlOptions.CommandTimeout(120); }); options.EnableSensitiveDataLogging(false); options.EnableDetailedErrors(false); - - // Suprimir warning de pending model changes em testes E2E - // Migrations são aplicadas em runtime e podem estar ligeiramente desatualizadas options.ConfigureWarnings(warnings => warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)); }); @@ -327,13 +328,14 @@ private async Task ApplyMigrationsAsync() try { - // Aplicar migrations para cada módulo - await ApplyMigrationForContext(services); - await ApplyMigrationForContext(services); - await ApplyMigrationForContext(services); - await ApplyMigrationForContext(services); - await ApplyMigrationForContext(services); - await ApplyMigrationForContext(services); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); + await MigrationTestHelper.ApplyMigrationForContext(services.GetRequiredService()); Console.WriteLine("✅ Database migrations applied successfully"); } @@ -344,29 +346,20 @@ private async Task ApplyMigrationsAsync() } } - private static async Task ApplyMigrationForContext(IServiceProvider services) where TContext : DbContext - { - var context = services.GetRequiredService(); - await context.Database.MigrateAsync(); - } - - /// - /// Limpa dados do banco entre testes para garantir isolamento. - /// Mantém schema/migrations, apenas remove dados. - /// public async Task CleanupDatabaseAsync() { using var scope = Services.CreateScope(); var services = scope.ServiceProvider; - // Limpar cada DbContext await CleanupContext(services); await CleanupContext(services); await CleanupContext(services); await CleanupContext(services); + await CleanupContext(services); + await CleanupContext(services); await CleanupContext(services); + await CleanupContext(services); - // Limpar o cache do Redis para evitar contaminação entre testes if (_redisContainer != null) { await _redisContainer.ExecAsync(new[] { "redis-cli", "FLUSHALL" }); @@ -378,7 +371,6 @@ private static async Task CleanupContext(IServiceProvider services) wh var context = services.GetRequiredService(); var tableNames = new List(); - // Collect all table names foreach (var entityType in context.Model.GetEntityTypes()) { var tableName = entityType.GetTableName(); @@ -386,7 +378,6 @@ private static async Task CleanupContext(IServiceProvider services) wh if (!string.IsNullOrEmpty(tableName)) { - // Build schema-qualified table name var qualifiedTableName = string.IsNullOrEmpty(schema) || schema == "public" ? $"\"{tableName}\"" : $"\"{schema}\".\"{tableName}\""; @@ -397,110 +388,63 @@ private static async Task CleanupContext(IServiceProvider services) wh if (tableNames.Count > 0) { - // Deduplicate table names (in case of shared tables or multiple entities mapping to same table) var uniqueTables = tableNames.Distinct().ToList(); var batchSql = $"TRUNCATE TABLE {string.Join(", ", uniqueTables)} CASCADE"; - -#pragma warning disable EF1002 // Risk of SQL injection - table/schema names come from EF metadata, not user input await context.Database.ExecuteSqlRawAsync(batchSql); -#pragma warning restore EF1002 } } public async ValueTask DisposeAsync() { ApiClient?.Dispose(); - - if (_factory != null) - { - await _factory.DisposeAsync(); - } - - // Do NOT dispose static containers here. They should run for the entire test session. - // If necessary, implement a Resource Reaper or rely on container cleanup (WithCleanUp(true)). + if (_factory != null) await _factory.DisposeAsync(); } - /// - /// Executa ação com scope de serviço para acesso direto ao banco - /// public async Task WithServiceScopeAsync(Func> action) { using var scope = Services.CreateScope(); return await action(scope.ServiceProvider); } - /// - /// Executa ação com scope de serviço para acesso direto ao banco - /// public async Task WithServiceScopeAsync(Func action) { using var scope = Services.CreateScope(); await action(scope.ServiceProvider); } - /// - /// Extrai o ID de um recurso do header Location de uma resposta HTTP 201 Created - /// public static Guid ExtractIdFromLocation(string locationHeader) { if (locationHeader.Contains("?id=")) { var uri = new Uri(locationHeader, UriKind.RelativeOrAbsolute); - var query = uri.Query.TrimStart('?'); - var idParam = query.Split('&').FirstOrDefault(p => p.StartsWith("id=")); - - if (idParam != null) - { - var idValue = idParam.Split('=')[1]; - return Guid.Parse(idValue); - } + var idParam = uri.Query.TrimStart('?').Split('&').FirstOrDefault(p => p.StartsWith("id=")); + if (idParam != null) return Guid.Parse(idParam.Split('=')[1]); } - - var segments = locationHeader.Split('/'); - return Guid.Parse(segments[^1]); + return Guid.Parse(locationHeader.Split('/')[^1]); } - /// - /// Configura autenticação como administrador. - /// Cria/reut iliza contexto AsyncLocal automaticamente. - /// public static void AuthenticateAsAdmin() { MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers.ConfigurableTestAuthenticationHandler.GetOrCreateTestContext(); MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers.ConfigurableTestAuthenticationHandler.ConfigureAdmin(); } - /// - /// Configura autenticação como usuário regular. - /// Cria/reutiliza contexto AsyncLocal automaticamente. - /// public static void AuthenticateAsUser(string userId = "test-user-id", string username = "testuser") { MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers.ConfigurableTestAuthenticationHandler.GetOrCreateTestContext(); MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers.ConfigurableTestAuthenticationHandler.ConfigureRegularUser(userId, username); } - /// - /// Remove autenticação (testes anônimos). - /// public static void AuthenticateAsAnonymous() { MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers.ConfigurableTestAuthenticationHandler.ClearConfiguration(); } - /// - /// Limpa o estado de autenticação. - /// IMPORTANTE: Chamar no início de cada teste para evitar vazamento de estado entre testes que compartilham fixture. - /// public static void BeforeEachTest() { MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers.ConfigurableTestAuthenticationHandler.ClearConfiguration(); } -#pragma warning disable CA2000 // Dispose objects before losing scope - StringContent is disposed by HttpClient - /// - /// Envia POST com JSON serializado - /// public async Task PostJsonAsync(string requestUri, T content) { var json = System.Text.Json.JsonSerializer.Serialize(content, JsonOptions); @@ -508,20 +452,13 @@ public async Task PostJsonAsync(string requestUri, T con return await ApiClient.PostAsync(requestUri, stringContent); } - /// - /// Envia PUT com JSON serializado - /// public async Task PutJsonAsync(string requestUri, T content) { var json = System.Text.Json.JsonSerializer.Serialize(content, JsonOptions); var stringContent = new StringContent(json, System.Text.Encoding.UTF8, new System.Net.Http.Headers.MediaTypeHeaderValue("application/json")); return await ApiClient.PutAsync(requestUri, stringContent); } -#pragma warning restore CA2000 - /// - /// Envia PATCH com JSON serializado - /// public async Task PatchJsonAsync(string requestUri, T content) { var json = System.Text.Json.JsonSerializer.Serialize(content, JsonOptions); @@ -529,18 +466,12 @@ public async Task PatchJsonAsync(string requestUri, T co return await ApiClient.PatchAsync(requestUri, stringContent); } - /// - /// Deserializa JSON da resposta HTTP - /// public static async Task ReadJsonAsync(HttpResponseMessage response) { var content = await response.Content.ReadAsStringAsync(); return System.Text.Json.JsonSerializer.Deserialize(content, JsonOptions); } - /// - /// Cria um usuário de teste e retorna seu ID - /// public async Task CreateTestUserAsync(string? username = null, string? email = null) { var uniqueId = Guid.NewGuid().ToString("N")[..8]; diff --git a/tests/MeAjudaAi.E2E.Tests/Infrastructure/SynchronousInMemoryMessageBus.cs b/tests/MeAjudaAi.E2E.Tests/Infrastructure/SynchronousInMemoryMessageBus.cs new file mode 100644 index 000000000..ba1656dc4 --- /dev/null +++ b/tests/MeAjudaAi.E2E.Tests/Infrastructure/SynchronousInMemoryMessageBus.cs @@ -0,0 +1,50 @@ +using MeAjudaAi.Shared.Events; +using MeAjudaAi.Shared.Messaging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace MeAjudaAi.E2E.Tests.Infrastructure; + +/// +/// Uma implementação simples de MessageBus em memória que executa os handlers sincronicamente. +/// Útil para testes E2E onde queremos validar consistência eventual de forma determinística. +/// +public class SynchronousInMemoryMessageBus( + IServiceProvider serviceProvider, + ILogger logger) : IMessageBus +{ + public Task SendAsync(TMessage message, string? queueName = null, CancellationToken cancellationToken = default) + { + return PublishAsync(message, queueName, cancellationToken); + } + + public async Task PublishAsync(TMessage @event, string? topicName = null, CancellationToken cancellationToken = default) + { + if (@event == null) return; + + logger.LogInformation("SynchronousInMemoryMessageBus: Dispatching {EventType}", typeof(TMessage).Name); + + // Resolve todos os IEventHandler registrados para este tipo de mensagem via Reflection + // para contornar a falta de restrições na interface IMessageBus + using var scope = serviceProvider.CreateScope(); + var handlerType = typeof(IEventHandler<>).MakeGenericType(typeof(TMessage)); + var handlers = scope.ServiceProvider.GetServices(handlerType); + + foreach (var handler in handlers) + { + if (handler == null) continue; + + var method = handlerType.GetMethod("HandleAsync"); + if (method != null) + { + await (Task)method.Invoke(handler, [@event, cancellationToken])!; + } + } + } + + public Task SubscribeAsync(Func handler, string? subscriptionName = null, CancellationToken cancellationToken = default) + { + // Não implementado para esta versão simplificada de teste + return Task.CompletedTask; + } +} diff --git a/tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj b/tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj index 5b018034f..09bf3e21d 100644 --- a/tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj +++ b/tests/MeAjudaAi.E2E.Tests/MeAjudaAi.E2E.Tests.csproj @@ -46,6 +46,10 @@ + + + + diff --git a/tests/MeAjudaAi.E2E.Tests/Modules/Ratings/RatingsEndToEndTests.cs b/tests/MeAjudaAi.E2E.Tests/Modules/Ratings/RatingsEndToEndTests.cs new file mode 100644 index 000000000..1e4fb3603 --- /dev/null +++ b/tests/MeAjudaAi.E2E.Tests/Modules/Ratings/RatingsEndToEndTests.cs @@ -0,0 +1,166 @@ +using System.Net; +using System.Net.Http.Json; +using FluentAssertions; +using MeAjudaAi.E2E.Tests.Base; +using MeAjudaAi.Modules.SearchProviders.Application.DTOs; +using MeAjudaAi.Modules.Providers.Application.DTOs; +using MeAjudaAi.Modules.Providers.Domain.Enums; +using MeAjudaAi.Contracts.Models; +using MeAjudaAi.Shared.Tests.TestInfrastructure.Handlers; +using Xunit; + +namespace MeAjudaAi.E2E.Tests.Modules.Ratings; + +[Trait("Category", "E2E")] +[Trait("Module", "Ratings")] +public class RatingsEndToEndTests : BaseTestContainerTest +{ + protected override bool EnableEventsAndMessageBus => true; + + private readonly ITestOutputHelper _output; + + public RatingsEndToEndTests(ITestOutputHelper output) + { + _output = output; + } + + public override async ValueTask InitializeAsync() + { + await base.InitializeAsync(); + await CleanupDatabaseAsync(); + } + + [Fact] + public async Task CreateReview_WithValidData_ShouldUpdateProviderRatingInSearch() + { + // Arrange + // Note: BaseTestContainerTest handles initialization and authentication setup + + // 1. Criar um prestador para ser avaliado + AuthenticateAsAdmin(); + var providerId = await CreateTestProviderAsync(); + + // 2. Autenticar como cliente (diferente do prestador) + AuthenticateAsAdmin(); + var customerId = await CreateTestUserAsync(); + AuthenticateAsUser(customerId.ToString()); + + var reviewRequest = new + { + providerId = providerId, + rating = 5, + comment = "" // Sem comentário para disparar auto-aprovação e atualizar busca + }; + + // Act - Criar a avaliação + AuthenticateAsUser(customerId.ToString()); + var response = await ApiClient.PostAsJsonAsync("/api/v1/ratings", reviewRequest); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.Created); + var reviewId = await response.Content.ReadFromJsonAsync(); + reviewId.Should().NotBeEmpty(); + + // 3. Verificar se a média foi atualizada no módulo de busca + // O SynchronousInMemoryMessageBus garante que o processamento ocorreu antes do retorno da API + // Usamos term=providerName para garantir que buscamos o prestador correto + var searchResponse = await ApiClient.GetAsync($"/api/v1/search/providers?latitude=-23.5505&longitude=-46.6333&radiusInKm=10"); + searchResponse.StatusCode.Should().Be(System.Net.HttpStatusCode.OK); + + var searchResult = await searchResponse.Content.ReadFromJsonAsync>(MeAjudaAi.Shared.Serialization.SerializationDefaults.Default); + var providerInSearch = searchResult?.Items.FirstOrDefault(p => p.ProviderId == providerId); + + providerInSearch.Should().NotBeNull(); + // Nota 5 deve ser a média (já que é a única avaliação) + providerInSearch!.AverageRating.Should().Be(5.0m); + providerInSearch.TotalReviews.Should().Be(1); + } + + private async Task CreateTestProviderAsync() + { + AuthenticateAsAdmin(); + + var userId = await CreateTestUserAsync(); + var name = $"Provider_{Guid.NewGuid():N}"; + var request = new + { + UserId = userId.ToString(), + Name = name, + Type = EProviderType.Individual, + BusinessProfile = new + { + LegalName = name, + FantasyName = name, + Description = $"Test provider {name}", + ContactInfo = new + { + Email = $"{name}@example.com", + PhoneNumber = "+5511999999999" + }, + PrimaryAddress = new + { + Street = "Avenida Paulista", + Number = "1578", + Neighborhood = "Bela Vista", + City = "São Paulo", + State = "SP", + ZipCode = "01310-200", + Country = "Brasil" + } + } + }; + + var response = await ApiClient.PostAsJsonAsync("/api/v1/providers", request); + response.EnsureSuccessStatusCode(); + + var location = response.Headers.Location?.ToString(); + var providerId = ExtractIdFromLocation(location!); + + // Não tentaremos verificar/ativar via REST API porque as regras de estado (PendingBasicInfo -> etc) + // requerem múltiplos endpoints que não são o foco deste teste. + // Faremos a inserção direta do provider no SearchProviders (como em SearchProvidersEndToEndTests) + // com rating = 0 para testar exclusivamente a integração de Reviews -> Search. + await InsertSearchableProviderAsync(providerId, name, -23.5505, -46.6333); + + return providerId; + } + + private async Task InsertSearchableProviderAsync(Guid providerId, string name, double latitude, double longitude) + { + await WithServiceScopeAsync(async sp => + { + var dapper = sp.GetRequiredService(); + + var sql = @" + INSERT INTO search_providers.searchable_providers + (id, provider_id, slug, name, description, city, state, location, average_rating, total_reviews, subscription_tier, service_ids, is_active, created_at, updated_at) + VALUES + (@Id, @ProviderId, @Slug, @Name, @Description, @City, @State, ST_SetSRID(ST_MakePoint(@Longitude, @Latitude), 4326)::geography, @AvgRating, @TotalReviews, @SubscriptionTier, @ServiceIds, @IsActive, @CreatedAt, @UpdatedAt) + ON CONFLICT (provider_id) + DO UPDATE SET + average_rating = EXCLUDED.average_rating, + total_reviews = EXCLUDED.total_reviews, + updated_at = CURRENT_TIMESTAMP"; + + await dapper.ExecuteAsync(sql, new + { + Id = Guid.NewGuid(), + ProviderId = providerId, + Slug = name.ToLowerInvariant().Replace(" ", "-").Replace("_", "-"), + Name = name, + Description = $"Test Provider {name}", + City = "São Paulo", + State = "SP", + Latitude = latitude, + Longitude = longitude, + AvgRating = 0.0m, // Inicializa com 0 + TotalReviews = 0, + SubscriptionTier = 1, // Standard + ServiceIds = Array.Empty(), + IsActive = true, + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTime.UtcNow + }); + }); + } +} diff --git a/tests/MeAjudaAi.E2E.Tests/packages.lock.json b/tests/MeAjudaAi.E2E.Tests/packages.lock.json index 3b998c546..62255bbad 100644 --- a/tests/MeAjudaAi.E2E.Tests/packages.lock.json +++ b/tests/MeAjudaAi.E2E.Tests/packages.lock.json @@ -1610,6 +1610,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1813,6 +1814,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/tests/MeAjudaAi.Integration.Tests/packages.lock.json b/tests/MeAjudaAi.Integration.Tests/packages.lock.json index 4a9ca257d..09fdb5f35 100644 --- a/tests/MeAjudaAi.Integration.Tests/packages.lock.json +++ b/tests/MeAjudaAi.Integration.Tests/packages.lock.json @@ -2529,6 +2529,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -2732,6 +2733,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": { diff --git a/tests/MeAjudaAi.Shared.Tests/packages.lock.json b/tests/MeAjudaAi.Shared.Tests/packages.lock.json index c97e2ed88..9ca44b929 100644 --- a/tests/MeAjudaAi.Shared.Tests/packages.lock.json +++ b/tests/MeAjudaAi.Shared.Tests/packages.lock.json @@ -1102,6 +1102,7 @@ "MeAjudaAi.Modules.Documents.API": "[1.0.0, )", "MeAjudaAi.Modules.Locations.API": "[1.0.0, )", "MeAjudaAi.Modules.Providers.API": "[1.0.0, )", + "MeAjudaAi.Modules.Ratings.API": "[1.0.0, )", "MeAjudaAi.Modules.SearchProviders.API": "[1.0.0, )", "MeAjudaAi.Modules.ServiceCatalogs.API": "[1.0.0, )", "MeAjudaAi.Modules.Users.API": "[1.0.0, )", @@ -1259,6 +1260,35 @@ "MeAjudaAi.Shared": "[1.0.0, )" } }, + "meajudaai.modules.ratings.api": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Infrastructure": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.application": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Domain": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Npgsql": "[10.0.2, )" + } + }, + "meajudaai.modules.ratings.domain": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Shared": "[1.0.0, )" + } + }, + "meajudaai.modules.ratings.infrastructure": { + "type": "Project", + "dependencies": { + "MeAjudaAi.Modules.Ratings.Application": "[1.0.0, )", + "Microsoft.EntityFrameworkCore": "[10.0.5, )", + "Microsoft.EntityFrameworkCore.Design": "[10.0.5, )", + "Npgsql.EntityFrameworkCore.PostgreSQL": "[10.0.1, )" + } + }, "meajudaai.modules.searchproviders.api": { "type": "Project", "dependencies": {